diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d62f9b6 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +bower.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9607c65 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +### UI Bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com) + +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/bootstrap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://secure.travis-ci.org/angular-ui/bootstrap.svg)](http://travis-ci.org/angular-ui/bootstrap) +[![devDependency Status](https://david-dm.org/angular-ui/bootstrap/dev-status.svg?branch=master)](https://david-dm.org/angular-ui/bootstrap#info=devDependencies) + +### Quick links +- [Demo](#demo) +- [Installation](#installation) + - [NPM](#install-with-npm) + - [Bower](#install-with-bower) + - [NuGet](#install-with-nuget) + - [Custom](#custom-build) + - [Manual](#manual-download) +- [Support](#support) + - [FAQ](#faq) + - [Supported browsers](#supported-browsers) + - [Need help?](#need-help) + - [Found a bug?](#found-a-bug) +- [Contributing to the project](#contributing-to-the-project) +- [Development, meeting minutes, roadmap and more.](#development-meeting-minutes-roadmap-and-more) + + +# Demo + +Do you want to see directives in action? Visit http://angular-ui.github.io/bootstrap/! + +# Installation + +Installation is easy as UI Bootstrap has minimal dependencies - only the AngularJS and Twitter Bootstrap's CSS are required. +Note: Since version 0.13.0, UI Bootstrap depends on [ngAnimate](https://docs.angularjs.org/api/ngAnimate) for transitions and animations, such as the accordion, carousel, etc. Include `ngAnimate` in the module dependencies for your app in order to enable animation. + +#### Install with NPM + +```sh +$ npm install angular-ui-bootstrap +``` + +This will install AngularJS and Bootstrap NPM packages. + +#### Install with Bower +```sh +$ bower install angular-bootstrap +``` + +Note: do not install 'angular-ui-bootstrap'. A separate repository - [bootstrap-bower](https://github.com/angular-ui/bootstrap-bower) - hosts the compiled javascript file and bower.json. + +#### Install with NuGet +To install AngularJS UI Bootstrap, run the following command in the Package Manager Console + +```sh +PM> Install-Package Angular.UI.Bootstrap +``` + +#### Custom build + +Head over to http://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it. + +#### Manual download + +After downloading dependencies (or better yet, referencing them from your favorite CDN) you need to download build version of this project. All the files and their purposes are described here: +https://github.com/angular-ui/bootstrap/tree/gh-pages#build-files +Don't worry, if you are not sure which file to take, opt for `ui-bootstrap-tpls-[version].min.js`. + +### Adding dependency to your project + +When you are done downloading all the dependencies and project files the only remaining part is to add dependencies on the `ui.bootstrap` AngularJS module: + +```js +angular.module('myModule', ['ui.bootstrap']); +``` + +If you're a Browserify or Webpack user, you can do: + +```js +var uibs = require('angular-ui-bootstrap'); + +angular.module('myModule', [uibs]); +``` + +# Support + +## FAQ + +https://github.com/angular-ui/bootstrap/wiki/FAQ + +## Supported browsers + +Directives from this repository are automatically tested with the following browsers: +* Chrome (stable and canary channel) +* Firefox +* IE 9 and 10 +* Opera +* Safari + +Modern mobile browsers should work without problems. + + +## Need help? +Need help using UI Bootstrap? + +* Live help in the IRC (`#angularjs` channel at the `freenode` network). Use this [webchat](https://webchat.freenode.net/) or your own IRC client. +* Ask a question in [StackOverflow](http://stackoverflow.com/) under the [angular-ui-bootstrap](http://stackoverflow.com/questions/tagged/angular-ui-bootstrap) tag. + +**Please do not create new issues in this repository to ask questions about using UI Bootstrap** + +## Found a bug? +Please take a look at [CONTRIBUTING.md](CONTRIBUTING.md#you-think-youve-found-a-bug) and submit your issue [here](https://github.com/angular-ui/bootstrap/issues/new). + + +---- + + +# Contributing to the project + +We are always looking for the quality contributions! Please check the [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution guidelines. + +# Development, meeting minutes, roadmap and more. + +Head over to the [Wiki](https://github.com/angular-ui/bootstrap/wiki) for notes on development for UI Bootstrap, meeting minutes from the UI Bootstrap team, roadmap plans, project philosophy and more. diff --git a/bower.json b/bower.json index ea65aad..05163be 100644 --- a/bower.json +++ b/bower.json @@ -11,9 +11,9 @@ "license": "MIT", "ignore": [], "description": "Native AngularJS (Angular) directives for Bootstrap.", - "version": "0.13.0", + "version": "2.5.0", "main": ["./ui-bootstrap-tpls.js"], "dependencies": { - "angular": ">=1.3.0" + "angular": ">=1.4.0" } } diff --git a/index.js b/index.js new file mode 100644 index 0000000..a174f26 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +require('./ui-bootstrap-tpls'); +module.exports = 'ui.bootstrap'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ff93d6 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "angular-ui-bootstrap", + "version": "2.5.0", + "description": "Bootstrap widgets for Angular", + "main": "index.js", + "homepage": "http://angular-ui.github.io/bootstrap/", + "repository": { + "type": "git", + "url": "https://github.com/angular-ui/bootstrap.git" + }, + "keywords": [ + "angular", + "bootstrap", + "angular-ui", + "components", + "client-side" + ], + "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", + "peerDependencies": { + "angular": ">= 1.4.0-beta.0 || >= 1.5.0-beta.0" + }, + "license": "MIT" +} diff --git a/ui-bootstrap-csp.css b/ui-bootstrap-csp.css index d772f78..3b69cf6 100644 --- a/ui-bootstrap-csp.css +++ b/ui-bootstrap-csp.css @@ -3,4 +3,113 @@ .ng-animate.item:not(.left):not(.right) { -webkit-transition: 0s ease-in-out left; transition: 0s ease-in-out left -} \ No newline at end of file +} +.uib-datepicker .uib-title { + width: 100%; +} + +.uib-day button, .uib-month button, .uib-year button { + min-width: 100%; +} + +.uib-left, .uib-right { + width: 100% +} + +.uib-position-measure { + display: block !important; + visibility: hidden !important; + position: absolute !important; + top: -9999px !important; + left: -9999px !important; +} + +.uib-position-scrollbar-measure { + position: absolute !important; + top: -9999px !important; + width: 50px !important; + height: 50px !important; + overflow: scroll !important; +} + +.uib-position-body-scrollbar-measure { + overflow: scroll !important; +} +.uib-datepicker-popup.dropdown-menu { + display: block; + float: none; + margin: 0; +} + +.uib-button-bar { + padding: 10px 9px 2px; +} + +[uib-tooltip-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-popover-popup].popover.top-left > .arrow, +[uib-popover-popup].popover.top-right > .arrow, +[uib-popover-popup].popover.bottom-left > .arrow, +[uib-popover-popup].popover.bottom-right > .arrow, +[uib-popover-popup].popover.left-top > .arrow, +[uib-popover-popup].popover.left-bottom > .arrow, +[uib-popover-popup].popover.right-top > .arrow, +[uib-popover-popup].popover.right-bottom > .arrow, +[uib-popover-html-popup].popover.top-left > .arrow, +[uib-popover-html-popup].popover.top-right > .arrow, +[uib-popover-html-popup].popover.bottom-left > .arrow, +[uib-popover-html-popup].popover.bottom-right > .arrow, +[uib-popover-html-popup].popover.left-top > .arrow, +[uib-popover-html-popup].popover.left-bottom > .arrow, +[uib-popover-html-popup].popover.right-top > .arrow, +[uib-popover-html-popup].popover.right-bottom > .arrow, +[uib-popover-template-popup].popover.top-left > .arrow, +[uib-popover-template-popup].popover.top-right > .arrow, +[uib-popover-template-popup].popover.bottom-left > .arrow, +[uib-popover-template-popup].popover.bottom-right > .arrow, +[uib-popover-template-popup].popover.left-top > .arrow, +[uib-popover-template-popup].popover.left-bottom > .arrow, +[uib-popover-template-popup].popover.right-top > .arrow, +[uib-popover-template-popup].popover.right-bottom > .arrow { + top: auto; + bottom: auto; + left: auto; + right: auto; + margin: 0; +} + +[uib-popover-popup].popover, +[uib-popover-html-popup].popover, +[uib-popover-template-popup].popover { + display: block !important; +} + +.uib-time input { + width: 50px; +} + +[uib-typeahead-popup].dropdown-menu { + display: block; +} diff --git a/ui-bootstrap-tpls.js b/ui-bootstrap-tpls.js index 9b8e8db..e0a9f53 100644 --- a/ui-bootstrap-tpls.js +++ b/ui-bootstrap-tpls.js @@ -2,52 +2,134 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.13.1-SNAPSHOT - 2015-10-06 + * Version: 2.5.0 - 2017-01-28 * License: MIT - */ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.transition","ui.bootstrap.typeahead"]); -angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-popup.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/tooltip/tooltip-template-popup.html","template/popover/popover-template.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); + */angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); +angular.module("ui.bootstrap.tpls", ["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]); angular.module('ui.bootstrap.collapse', []) - .directive('collapse', ['$animate', function ($animate) { - + .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { + var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + var expandingExpr = $parse(attrs.expanding), + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed), + horizontal = false, + css = {}, + cssTo = {}; + + init(); + + function init() { + horizontal = !!('horizontal' in attrs); + if (horizontal) { + css = { + width: '' + }; + cssTo = {width: '0'}; + } else { + css = { + height: '' + }; + cssTo = {height: '0'}; + } + if (!scope.$eval(attrs.uibCollapse)) { + element.addClass('in') + .addClass('collapse') + .attr('aria-expanded', true) + .attr('aria-hidden', false) + .css(css); + } + } + + function getScrollFromElement(element) { + if (horizontal) { + return {width: element.scrollWidth + 'px'}; + } + return {height: element.scrollHeight + 'px'}; + } + function expand() { - element.removeClass('collapse').addClass('collapsing'); - $animate.addClass(element, 'in', { - to: { height: element[0].scrollHeight + 'px' } - }).then(expandDone); + if (element.hasClass('collapse') && element.hasClass('in')) { + return; + } + + $q.resolve(expandingExpr(scope)) + .then(function() { + element.removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', true) + .attr('aria-hidden', false); + + if ($animateCss) { + $animateCss(element, { + addClass: 'in', + easing: 'ease', + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) + }).start()['finally'](expandDone); + } else { + $animate.addClass(element, 'in', { + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) + }).then(expandDone); + } + }, angular.noop); } function expandDone() { - element.removeClass('collapsing'); - element.css({height: 'auto'}); + element.removeClass('collapsing') + .addClass('collapse') + .css(css); + expandedExpr(scope); } function collapse() { - element - // IMPORTANT: The height must be set before adding "collapsing" class. - // Otherwise, the browser attempts to animate from height 0 (in - // collapsing class) to the given height here. - .css({height: element[0].scrollHeight + 'px'}) - // initially all panel collapse have the collapse class, this removal - // prevents the animation from jumping to collapsed state - .removeClass('collapse') - .addClass('collapsing'); + if (!element.hasClass('collapse') && !element.hasClass('in')) { + return collapseDone(); + } - $animate.removeClass(element, 'in', { - to: {height: '0'} - }).then(collapseDone); + $q.resolve(collapsingExpr(scope)) + .then(function() { + element + // IMPORTANT: The width must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from width 0 (in + // collapsing class) to the given width here. + .css(getScrollFromElement(element[0])) + // initially all panel collapse have the collapse class, this removal + // prevents the animation from jumping to collapsed state + .removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', false) + .attr('aria-hidden', true); + + if ($animateCss) { + $animateCss(element, { + removeClass: 'in', + to: cssTo + }).start()['finally'](collapseDone); + } else { + $animate.removeClass(element, 'in', { + to: cssTo + }).then(collapseDone); + } + }, angular.noop); } function collapseDone() { - element.css({height: '0'}); // Required so that collapse works when animation is disabled - element.removeClass('collapsing'); - element.addClass('collapse'); + element.css(cssTo); // Required so that collapse works when animation is disabled + element.removeClass('collapsing') + .addClass('collapse'); + collapsedExpr(scope); } - scope.$watch(attrs.collapse, function (shouldCollapse) { + scope.$watch(attrs.uibCollapse, function(shouldCollapse) { if (shouldCollapse) { collapse(); } else { @@ -58,23 +140,36 @@ angular.module('ui.bootstrap.collapse', []) }; }]); -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) +angular.module('ui.bootstrap.tabindex', []) + +.directive('uibTabindexToggle', function() { + return { + restrict: 'A', + link: function(scope, elem, attrs) { + attrs.$observe('disabled', function(disabled) { + attrs.$set('tabindex', disabled ? -1 : null); + }); + } + }; +}); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex']) -.constant('accordionConfig', { +.constant('uibAccordionConfig', { closeOthers: true }) -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - +.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? + $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if (closeOthers) { + angular.forEach(this.groups, function(group) { + if (group !== openGroup) { group.isOpen = false; } }); @@ -86,7 +181,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) var that = this; this.groups.push(groupScope); - groupScope.$on('$destroy', function (event) { + groupScope.$on('$destroy', function(event) { that.removeGroup(groupScope); }); }; @@ -94,35 +189,37 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); - if ( index !== -1 ) { + if (index !== -1) { this.groups.splice(index, 1); } }; - }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. -.directive('accordion', function () { +.directive('uibAccordion', function() { return { - restrict:'EA', - controller:'AccordionController', + controller: 'UibAccordionController', + controllerAs: 'accordion', transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion.html'; + } }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', function() { +.directive('uibAccordionGroup', function() { return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', + require: '^uibAccordion', // We need this directive to be inside an accordion + transclude: true, // It transcludes the contents of the directive into the template + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; + }, scope: { heading: '@', // Interpolate the heading attribute onto this scope + panelClass: '@?', // Ditto with panelClass isOpen: '=?', isDisabled: '=?' }, @@ -132,35 +229,41 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) }; }, link: function(scope, element, attrs, accordionCtrl) { + element.addClass('panel'); accordionCtrl.addGroup(scope); + scope.openClass = attrs.openClass || 'panel-open'; + scope.panelClass = attrs.panelClass || 'panel-default'; scope.$watch('isOpen', function(value) { - if ( value ) { + element.toggleClass(scope.openClass, !!value); + if (value) { accordionCtrl.closeOthers(scope); } }); - scope.toggleOpen = function() { - if ( !scope.isDisabled ) { - scope.isOpen = !scope.isOpen; + scope.toggleOpen = function($event) { + if (!scope.isDisabled) { + if (!$event || $event.which === 32) { + scope.isOpen = !scope.isOpen; + } } }; + + var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + scope.headingId = id + '-tab'; + scope.panelId = id + '-panel'; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { +.directive('uibAccordionHeading', function() { return { - restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, - require: '^accordionGroup', - link: function(scope, element, attr, accordionGroupCtrl, transclude) { + require: '^uibAccordionGroup', + link: function(scope, element, attrs, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] @@ -171,114 +274,129 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element -//
-//
...
-// ... -//
-.directive('accordionTransclude', function() { +.directive('uibAccordionTransclude', function() { return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); + require: '^uibAccordionGroup', + link: function(scope, element, attrs, controller) { + scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { + if (heading) { + var elem = angular.element(element[0].querySelector(getHeaderSelectors())); + elem.html(''); + elem.append(heading); } }); } }; -}) -; + function getHeaderSelectors() { + return 'uib-accordion-header,' + + 'data-uib-accordion-header,' + + 'x-uib-accordion-header,' + + 'uib\\:accordion-header,' + + '[uib-accordion-header],' + + '[data-uib-accordion-header],' + + '[x-uib-accordion-header]'; + } +}); angular.module('ui.bootstrap.alert', []) -.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { - $scope.closeable = 'close' in $attrs; - this.close = $scope.close; +.controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) { + $scope.closeable = !!$attrs.close; + $element.addClass('alert'); + $attrs.$set('role', 'alert'); + if ($scope.closeable) { + $element.addClass('alert-dismissible'); + } + + var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? + $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; + + if (dismissOnTimeout) { + $timeout(function() { + $scope.close(); + }, parseInt(dismissOnTimeout, 10)); + } }]) -.directive('alert', function () { +.directive('uibAlert', function() { return { - restrict:'EA', - controller:'AlertController', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, + controller: 'UibAlertController', + controllerAs: 'alert', + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/alert/alert.html'; + }, + transclude: true, scope: { - type: '@', close: '&' } }; -}) - -.directive('dismissOnTimeout', ['$timeout', function($timeout) { - return { - require: 'alert', - link: function(scope, element, attrs, alertCtrl) { - $timeout(function(){ - alertCtrl.close(); - }, parseInt(attrs.dismissOnTimeout, 10)); - } - }; -}]); - -angular.module('ui.bootstrap.bindHtml', []) +}); - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); angular.module('ui.bootstrap.buttons', []) -.constant('buttonConfig', { +.constant('uibButtonConfig', { activeClass: 'active', toggleEvent: 'click' }) -.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { +.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) -.directive('btnRadio', function () { +.directive('uibBtnRadio', ['$parse', function($parse) { return { - require: ['btnRadio', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnRadio', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'buttons', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + var uncheckableExpr = $parse(attrs.uibUncheckable); + + element.find('input').css({display: 'none'}); //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + ngModelCtrl.$render = function() { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio))); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + var isActive = element.hasClass(buttonsCtrl.activeClass); if (!isActive || angular.isDefined(attrs.uncheckable)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + scope.$apply(function() { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio)); ngModelCtrl.$render(); }); } }); + + if (attrs.uibUncheckable) { + scope.$watch(uncheckableExpr, function(uncheckable) { + attrs.$set('uncheckable', uncheckable ? '' : undefined); + }); + } } }; -}) +}]) -.directive('btnCheckbox', function () { +.directive('uibBtnCheckbox', function() { return { - require: ['btnCheckbox', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnCheckbox', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'button', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + element.find('input').css({display: 'none'}); + function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } @@ -287,19 +405,22 @@ angular.module('ui.bootstrap.buttons', []) return getCheckboxValue(attrs.btnCheckboxFalse, false); } - function getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); - return angular.isDefined(val) ? val : defaultValue; + function getCheckboxValue(attribute, defaultValue) { + return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; } //model -> UI - ngModelCtrl.$render = function () { + ngModelCtrl.$render = function() { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - scope.$apply(function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + + scope.$apply(function() { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); @@ -308,118 +429,237 @@ angular.module('ui.bootstrap.buttons', []) }; }); -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ angular.module('ui.bootstrap.carousel', []) -.controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) { + +.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) { var self = this, slides = self.slides = $scope.slides = [], - NO_TRANSITION = 'uib-noTransition', SLIDE_DIRECTION = 'uib-slideDirection', - currentIndex = -1, + currentIndex = $scope.active, currentInterval, isPlaying; - self.currentSlide = null; var destroyed = false; + $element.addClass('carousel'); + + self.addSlide = function(slide, element) { + slides.push({ + slide: slide, + element: element + }); + slides.sort(function(a, b) { + return +a.slide.index - +b.slide.index; + }); + //if this is the first slide or the slide is set to active, select it + if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) { + if ($scope.$currentTransition) { + $scope.$currentTransition = null; + } + + currentIndex = slide.index; + $scope.active = slide.index; + setActive(currentIndex); + self.select(slides[findSlideIndex(slide)]); + if (slides.length === 1) { + $scope.play(); + } + } + }; + + self.getCurrentIndex = function() { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === currentIndex) { + return i; + } + } + }; + + self.next = $scope.next = function() { + var newIndex = (self.getCurrentIndex() + 1) % slides.length; + + if (newIndex === 0 && $scope.noWrap()) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'next'); + }; + + self.prev = $scope.prev = function() { + var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + + if ($scope.noWrap() && newIndex === slides.length - 1) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'prev'); + }; + + self.removeSlide = function(slide) { + var index = findSlideIndex(slide); + + //get the index of the slide inside the carousel + slides.splice(index, 1); + if (slides.length > 0 && currentIndex === index) { + if (index >= slides.length) { + currentIndex = slides.length - 1; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[slides.length - 1]); + } else { + currentIndex = index; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + $scope.active = currentIndex; + } + + //clean the active value when no more slide + if (slides.length === 0) { + currentIndex = null; + $scope.active = null; + } + }; + /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = self.indexOfSlide(nextSlide); + var nextIndex = findSlideIndex(nextSlide.slide); //Decide direction if it's not given if (direction === undefined) { direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; } //Prevent this user-triggered transition from occurring if there is already one in progress - if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) { - goNext(nextSlide, nextIndex, direction); + if (nextSlide.slide.index !== currentIndex && + !$scope.$currentTransition) { + goNext(nextSlide.slide, nextIndex, direction); } }; - function goNext(slide, index, direction) { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - - angular.extend(slide, {direction: direction, active: true}); - angular.extend(self.currentSlide || {}, {direction: direction, active: false}); - if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition && - slide.$element) { - slide.$element.data(SLIDE_DIRECTION, slide.direction); - $scope.$currentTransition = true; - slide.$element.one('$animate:close', function closeFn() { - $scope.$currentTransition = null; - }); - } + /* Allow outside people to call indexOf on slides array */ + $scope.indexOfSlide = function(slide) { + return +slide.slide.index; + }; - self.currentSlide = slide; - currentIndex = index; + $scope.isActive = function(slide) { + return $scope.active === slide.slide.index; + }; - // every time you change the index, prefetch adjacent slides - // $scope.$evalAsync(self.preFetchSlides); - $scope.$evalAsync(function () { - angular.extend(getSlideByIndex(self.getCurrentIndex()),{fetch: true}); - }); + $scope.isPrevDisabled = function() { + return $scope.active === 0 && $scope.noWrap(); + }; - //every time you change slides, reset the timer - restartTimer(); - } + $scope.isNextDisabled = function() { + return $scope.active === slides.length - 1 && $scope.noWrap(); + }; - $scope.$on('$destroy', function () { + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + + $element.on('mouseenter', $scope.pause); + $element.on('mouseleave', $scope.play); + + $scope.$on('$destroy', function() { destroyed = true; + resetTimer(); }); - function getSlideByIndex(index) { - if (angular.isUndefined(slides[index].index)) { - return slides[index]; + $scope.$watch('noTransition', function(noTransition) { + $animate.enabled($element, !noTransition); + }); + + $scope.$watch('interval', restartTimer); + + $scope.$watchCollection('slides', resetTransition); + + $scope.$watch('active', function(index) { + if (angular.isNumber(index) && currentIndex !== index) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === index) { + index = i; + break; + } + } + + var slide = slides[index]; + if (slide) { + setActive(index); + self.select(slides[index]); + currentIndex = index; + } } - var i, len = slides.length; - for (i = 0; i < slides.length; ++i) { - if (slides[i].index == index) { + }); + + function getSlideByIndex(index) { + for (var i = 0, l = slides.length; i < l; ++i) { + if (slides[i].index === index) { return slides[i]; } } } - self.getCurrentIndex = function() { - if (self.currentSlide && angular.isDefined(self.currentSlide.index)) { - return +self.currentSlide.index; + function setActive(index) { + for (var i = 0; i < slides.length; i++) { + slides[i].slide.active = i === index; } - return currentIndex; - }; + } - /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide); - }; + function goNext(slide, index, direction) { + if (destroyed) { + return; + } - $scope.next = function() { - var newIndex = (self.getCurrentIndex() + 1) % slides.length; + angular.extend(slide, {direction: direction}); + angular.extend(slides[currentIndex].slide || {}, {direction: direction}); + if ($animate.enabled($element) && !$scope.$currentTransition && + slides[index].element && self.slides.length > 1) { + slides[index].element.data(SLIDE_DIRECTION, slide.direction); + var currentIdx = self.getCurrentIndex(); - return self.select(getSlideByIndex(newIndex), 'next'); - }; + if (angular.isNumber(currentIdx) && slides[currentIdx].element) { + slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction); + } - $scope.prev = function() { - var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + $scope.$currentTransition = true; + $animate.on('addClass', slides[index].element, function(element, phase) { + if (phase === 'close') { + $scope.$currentTransition = null; + $animate.off('addClass', element); + } + }); + } - return self.select(getSlideByIndex(newIndex), 'prev'); - }; + $scope.active = slide.index; + currentIndex = slide.index; + setActive(index); - $scope.isActive = function(slide) { - return self.currentSlide === slide; - }; + // every time you change the index, prefetch adjacent slides + $scope.$evalAsync(function () { + angular.extend(getSlideByIndex(self.getCurrentIndex()),{fetch: true}); + }); - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); + //every time you change slides, reset the timer + restartTimer(); + } - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval > 0) { - currentInterval = $interval(timerFn, interval); + function findSlideIndex(slide) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide === slide) { + return i; + } } } @@ -430,6 +670,20 @@ angular.module('ui.bootstrap.carousel', []) } } + function resetTransition(slides) { + if (!slides.length) { + $scope.$currentTransition = null; + } + } + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + function timerFn() { var interval = +$scope.interval; if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { @@ -438,346 +692,419 @@ angular.module('ui.bootstrap.carousel', []) $scope.pause(); } } +}]) - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); +.directive('uibCarousel', function() { + return { + transclude: true, + controller: 'UibCarouselController', + controllerAs: 'carousel', + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/carousel.html'; + }, + scope: { + active: '=', + interval: '=', + noTransition: '=', + noPause: '=', + noWrap: '&' } }; - $scope.pause = function() { - if (!$scope.noPause) { - isPlaying = false; - resetTimer(); +}) + +.directive('uibSlide', ['$animate', function($animate) { + return { + require: '^uibCarousel', + restrict: 'A', + transclude: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/slide.html'; + }, + scope: { + actual: '=?', + index: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + element.addClass('item'); + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + $animate[active ? 'addClass' : 'removeClass'](element, 'active'); + }); } }; +}]) - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; - } - }; - - self.removeSlide = function(slide) { - if (angular.isDefined(slide.index)) { - slides.sort(function(a, b) { - return +a.index > +b.index; - }); - } - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { - self.select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - }; - - self.preFetchSlides = function () { - var fetchIndex = self.getCurrentIndex(); - var fwdIndex = (fetchIndex + 1) % slides.length; - var revIndex = fetchIndex - 1 < 0 ? slides.length - 1 : fetchIndex - 1; - angular.extend(getSlideByIndex(fetchIndex),{fetch: true}); - angular.extend(getSlideByIndex(fwdIndex),{fetch: true}); - angular.extend(getSlideByIndex(revIndex),{fetch: true}); - }; - - $scope.$watch('noTransition', function(noTransition) { - $element.data(NO_TRANSITION, noTransition); - }); - -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', - scope: { - interval: '=', - noTransition: '=', - noPause: '=' - } - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * @param {number=} index The index of the slide. The slides will be sorted by this parameter. - * - * @example - - -
- - - - - - - Interval, in milliseconds: -
Enter a negative number to stop the interval. -
-
- -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
-*/ - -.directive('slide', function() { - return { - require: '^carousel', - restrict: 'EA', - transclude: true, - replace: true, - templateUrl: 'template/carousel/slide.html', - scope: { - active: '=?', - index: '=?', - fetch: '=?' - }, - link: function (scope, element, attrs, carouselCtrl) { - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('$destroy', function() { - carouselCtrl.removeSlide(scope); - }); +.animation('.item', ['$animateCss', +function($animateCss) { + var SLIDE_DIRECTION = 'uib-slideDirection'; - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); + function removeClass(element, className, callback) { + element.removeClass(className); + if (callback) { + callback(); } - }; -}) - -.animation('.item', [ - '$animate', -function ($animate) { - var NO_TRANSITION = 'uib-noTransition', - SLIDE_DIRECTION = 'uib-slideDirection'; + } return { - beforeAddClass: function (element, className, done) { - // Due to transclusion, noTransition property is on parent's scope - if (className == 'active' && element.parent() && - !element.parent().data(NO_TRANSITION)) { + beforeAddClass: function(element, className, done) { + if (className === 'active') { var stopped = false; var direction = element.data(SLIDE_DIRECTION); - var directionClass = direction == 'next' ? 'left' : 'right'; + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, + directionClass + ' ' + direction, done); element.addClass(direction); - $animate.addClass(element, directionClass).then(function () { - if (!stopped) { - element.removeClass(directionClass + ' ' + direction); - } - done(); - }); - return function () { + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { stopped = true; }; } done(); }, beforeRemoveClass: function (element, className, done) { - // Due to transclusion, noTransition property is on parent's scope - if (className == 'active' && element.parent() && - !element.parent().data(NO_TRANSITION)) { + if (className === 'active') { var stopped = false; var direction = element.data(SLIDE_DIRECTION); - var directionClass = direction == 'next' ? 'left' : 'right'; - $animate.addClass(element, directionClass).then(function () { - if (!stopped) { - element.removeClass(directionClass); - } - done(); - }); - return function () { + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, directionClass, done); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { stopped = true; }; } done(); } }; - -}]) - - -; +}]); angular.module('ui.bootstrap.dateparser', []) -.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) { // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; - this.parsers = {}; + var localeId; + var formatCodeToRegex; - var formatCodeToRegex = { - 'yyyy': { - regex: '\\d{4}', - apply: function(value) { this.year = +value; } - }, - 'yy': { - regex: '\\d{2}', - apply: function(value) { this.year = +value + 2000; } - }, - 'y': { - regex: '\\d{1,4}', - apply: function(value) { this.year = +value; } - }, - 'MMMM': { - regex: $locale.DATETIME_FORMATS.MONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } - }, - 'MMM': { - regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } - }, - 'MM': { - regex: '0[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'M': { - regex: '[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'dd': { - regex: '[0-2][0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'd': { - regex: '[1-2]?[0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'EEEE': { - regex: $locale.DATETIME_FORMATS.DAY.join('|') - }, - 'EEE': { - regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') - }, - 'HH': { - regex: '(?:0|1)[0-9]|2[0-3]', - apply: function(value) { this.hours = +value; } - }, - 'H': { - regex: '1?[0-9]|2[0-3]', - apply: function(value) { this.hours = +value; } - }, - 'mm': { - regex: '[0-5][0-9]', - apply: function(value) { this.minutes = +value; } - }, - 'm': { - regex: '[0-9]|[1-5][0-9]', - apply: function(value) { this.minutes = +value; } - }, - 'sss': { - regex: '[0-9][0-9][0-9]', - apply: function(value) { this.milliseconds = +value; } - }, - 'ss': { - regex: '[0-5][0-9]', - apply: function(value) { this.seconds = +value; } - }, - 's': { - regex: '[0-9]|[1-5][0-9]', - apply: function(value) { this.seconds = +value; } + this.init = function() { + localeId = $locale.id; + + this.parsers = {}; + this.formatters = {}; + + formatCodeToRegex = [ + { + key: 'yyyy', + regex: '\\d{4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yyyy'); + } + }, + { + key: 'yy', + regex: '\\d{2}', + apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yy'); + } + }, + { + key: 'y', + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'y'); + } + }, + { + key: 'M!', + regex: '0?[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { + var value = date.getMonth(); + if (/^[0-9]$/.test(value)) { + return dateFilter(date, 'MM'); + } + + return dateFilter(date, 'M'); + } + }, + { + key: 'MMMM', + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMMM'); } + }, + { + key: 'MMM', + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMM'); } + }, + { + key: 'MM', + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'MM'); } + }, + { + key: 'M', + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'M'); } + }, + { + key: 'd!', + regex: '[0-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { + var value = date.getDate(); + if (/^[1-9]$/.test(value)) { + return dateFilter(date, 'dd'); + } + + return dateFilter(date, 'd'); + } + }, + { + key: 'dd', + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'dd'); } + }, + { + key: 'd', + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'd'); } + }, + { + key: 'EEEE', + regex: $locale.DATETIME_FORMATS.DAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEEE'); } + }, + { + key: 'EEE', + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEE'); } + }, + { + key: 'HH', + regex: '(?:0|1)[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'HH'); } + }, + { + key: 'hh', + regex: '0[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'hh'); } + }, + { + key: 'H', + regex: '1?[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'H'); } + }, + { + key: 'h', + regex: '[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'h'); } + }, + { + key: 'mm', + regex: '[0-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'mm'); } + }, + { + key: 'm', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'm'); } + }, + { + key: 'sss', + regex: '[0-9][0-9][0-9]', + apply: function(value) { this.milliseconds = +value; }, + formatter: function(date) { return dateFilter(date, 'sss'); } + }, + { + key: 'ss', + regex: '[0-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 'ss'); } + }, + { + key: 's', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 's'); } + }, + { + key: 'a', + regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), + apply: function(value) { + if (this.hours === 12) { + this.hours = 0; + } + + if (value === 'PM') { + this.hours += 12; + } + }, + formatter: function(date) { return dateFilter(date, 'a'); } + }, + { + key: 'Z', + regex: '[+-]\\d{4}', + apply: function(value) { + var matches = value.match(/([+-])(\d{2})(\d{2})/), + sign = matches[1], + hours = matches[2], + minutes = matches[3]; + this.hours += toInt(sign + hours); + this.minutes += toInt(sign + minutes); + }, + formatter: function(date) { + return dateFilter(date, 'Z'); + } + }, + { + key: 'ww', + regex: '[0-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'ww'); } + }, + { + key: 'w', + regex: '[0-9]|[1-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'w'); } + }, + { + key: 'GGGG', + regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'), + formatter: function(date) { return dateFilter(date, 'GGGG'); } + }, + { + key: 'GGG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GGG'); } + }, + { + key: 'GG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GG'); } + }, + { + key: 'G', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'G'); } + } + ]; + + if (angular.version.major >= 1 && angular.version.minor > 4) { + formatCodeToRegex.push({ + key: 'LLLL', + regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'LLLL'); } + }); } }; + this.init(); + + function getFormatCodeToRegex(key) { + return filterFilter(formatCodeToRegex, {key: key}, true)[0]; + } + + this.getParser = function (key) { + var f = getFormatCodeToRegex(key); + return f && f.apply || null; + }; + + this.overrideParser = function (key, parser) { + var f = getFormatCodeToRegex(key); + if (f && angular.isFunction(parser)) { + this.parsers = {}; + f.apply = parser; + } + }.bind(this); + function createParser(format) { var map = [], regex = format.split(''); - angular.forEach(formatCodeToRegex, function(data, code) { - var index = format.indexOf(code); + // check for literal values + var quoteIndex = format.indexOf('\''); + if (quoteIndex > -1) { + var inLiteral = false; + format = format.split(''); + for (var i = quoteIndex; i < format.length; i++) { + if (inLiteral) { + if (format[i] === '\'') { + if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote + format[i+1] = '$'; + regex[i+1] = ''; + } else { // end of literal + regex[i] = ''; + inLiteral = false; + } + } + format[i] = '$'; + } else { + if (format[i] === '\'') { // start of literal + format[i] = '$'; + regex[i] = ''; + inLiteral = true; + } + } + } + + format = format.join(''); + } + + angular.forEach(formatCodeToRegex, function(data) { + var index = format.indexOf(data.key); if (index > -1) { format = format.split(''); regex[index] = '(' + data.regex + ')'; format[index] = '$'; // Custom symbol to define consumed part of format - for (var i = index + 1, n = index + code.length; i < n; i++) { + for (var i = index + 1, n = index + data.key.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); - map.push({ index: index, apply: data.apply }); + map.push({ + index: index, + key: data.key, + apply: data.apply, + matcher: data.regex + }); } }); @@ -787,26 +1114,116 @@ angular.module('ui.bootstrap.dateparser', []) }; } + function createFormatter(format) { + var formatters = []; + var i = 0; + var formatter, literalIdx; + while (i < format.length) { + if (angular.isNumber(literalIdx)) { + if (format.charAt(i) === '\'') { + if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') { + formatters.push(constructLiteralFormatter(format, literalIdx, i)); + literalIdx = null; + } + } else if (i === format.length) { + while (literalIdx < format.length) { + formatter = constructFormatterFromIdx(format, literalIdx); + formatters.push(formatter); + literalIdx = formatter.endIdx; + } + } + + i++; + continue; + } + + if (format.charAt(i) === '\'') { + literalIdx = i; + i++; + continue; + } + + formatter = constructFormatterFromIdx(format, i); + + formatters.push(formatter.parser); + i = formatter.endIdx; + } + + return formatters; + } + + function constructLiteralFormatter(format, literalIdx, endIdx) { + return function() { + return format.substr(literalIdx + 1, endIdx - literalIdx - 1); + }; + } + + function constructFormatterFromIdx(format, i) { + var currentPosStr = format.substr(i); + for (var j = 0; j < formatCodeToRegex.length; j++) { + if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) { + var data = formatCodeToRegex[j]; + return { + endIdx: i + data.key.length, + parser: data.formatter + }; + } + } + + return { + endIdx: i + 1, + parser: function() { + return currentPosStr.charAt(0); + } + }; + } + + this.filter = function(date, format) { + if (!angular.isDate(date) || isNaN(date) || !format) { + return ''; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.formatters[format]) { + this.formatters[format] = createFormatter(format); + } + + var formatters = this.formatters[format]; + + return formatters.reduce(function(str, formatter) { + return str + formatter(date); + }, ''); + }; + this.parse = function(input, format, baseDate) { - if ( !angular.isString(input) || !format ) { + if (!angular.isString(input) || !format) { return input; } format = $locale.DATETIME_FORMATS[format] || format; format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); - if ( !this.parsers[format] ) { - this.parsers[format] = createParser(format); + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.parsers[format]) { + this.parsers[format] = createParser(format, 'apply'); } var parser = this.parsers[format], regex = parser.regex, map = parser.map, - results = input.match(regex); - - if ( results && results.length ) { + results = input.match(regex), + tzOffset = false; + if (results && results.length) { var fields, dt; - if (baseDate) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { fields = { year: baseDate.getFullYear(), month: baseDate.getMonth(), @@ -817,19 +1234,40 @@ angular.module('ui.bootstrap.dateparser', []) milliseconds: baseDate.getMilliseconds() }; } else { + if (baseDate) { + $log.warn('dateparser:', 'baseDate is not a valid date'); + } fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; } - for( var i = 1, n = results.length; i < n; i++ ) { - var mapper = map[i-1]; - if ( mapper.apply ) { + for (var i = 1, n = results.length; i < n; i++) { + var mapper = map[i - 1]; + if (mapper.matcher === 'Z') { + tzOffset = true; + } + + if (mapper.apply) { mapper.apply.call(fields, results[i]); } } - if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds, - fields.milliseconds || 0); + var datesetter = tzOffset ? Date.prototype.setUTCFullYear : + Date.prototype.setFullYear; + var timesetter = tzOffset ? Date.prototype.setUTCHours : + Date.prototype.setHours; + + if (isValid(fields.year, fields.month, fields.date)) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) { + dt = new Date(baseDate); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours, fields.minutes, + fields.seconds, fields.milliseconds); + } else { + dt = new Date(0); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours || 0, fields.minutes || 0, + fields.seconds || 0, fields.milliseconds || 0); + } } return dt; @@ -843,230 +1281,304 @@ angular.module('ui.bootstrap.dateparser', []) return false; } - if ( month === 1 && date > 28) { - return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + if (month === 1 && date > 28) { + return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0); } - if ( month === 3 || month === 5 || month === 8 || month === 10) { - return date < 31; + if (month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; } return true; } -}]); -angular.module('ui.bootstrap.position', []) + function toInt(str) { + return parseInt(str, 10); + } -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { + this.toTimezone = toTimezone; + this.fromTimezone = fromTimezone; + this.timezoneToOffset = timezoneToOffset; + this.addDateMinutes = addDateMinutes; + this.convertTimezoneToLocal = convertTimezoneToLocal; - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } + function toTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone) : date; + } - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } + function fromTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date; + } - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; + //https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207 + function timezoneToOffset(timezone, fallback) { + timezone = timezone.replace(/:/g, ''); + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + } - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; - } + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; + } - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); + } +}]); - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, +// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to +// at most one element. +angular.module('ui.bootstrap.isClass', []) +.directive('uibIsClass', [ + '$animate', +function ($animate) { + // 11111111 22222222 + var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/; + // 11111111 22222222 + var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/; - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + var dataPerTracked = {}; - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + return { + restrict: 'A', + compile: function(tElement, tAttrs) { + var linkedScopes = []; + var instances = []; + var expToData = {}; + var lastActivated = null; + var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP); + var onExp = onExpMatches[2]; + var expsStr = onExpMatches[1]; + var exps = expsStr.split(','); + + return linkFn; + + function linkFn(scope, element, attrs) { + linkedScopes.push(scope); + instances.push({ + scope: scope, + element: element + }); - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; + exps.forEach(function(exp, k) { + addForExp(exp, scope); + }); - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + scope.$on('$destroy', removeScope); + } - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); + function addForExp(exp, scope) { + var matches = exp.match(IS_REGEXP); + var clazz = scope.$eval(matches[1]); + var compareWithExp = matches[2]; + var data = expToData[exp]; + if (!data) { + var watchFn = function(compareWithVal) { + var newActivated = null; + instances.some(function(instance) { + var thisVal = instance.scope.$eval(onExp); + if (thisVal === compareWithVal) { + newActivated = instance; + return true; + } + }); + if (data.lastActivated !== newActivated) { + if (data.lastActivated) { + $animate.removeClass(data.lastActivated.element, clazz); + } + if (newActivated) { + $animate.addClass(newActivated.element, clazz); + } + data.lastActivated = newActivated; + } + }; + expToData[exp] = data = { + lastActivated: null, + scope: scope, + watchFn: watchFn, + compareWithExp: compareWithExp, + watcher: scope.$watch(compareWithExp, watchFn) + }; + } + data.watchFn(scope.$eval(compareWithExp)); + } - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; + function removeScope(e) { + var removedScope = e.targetScope; + var index = linkedScopes.indexOf(removedScope); + linkedScopes.splice(index, 1); + instances.splice(index, 1); + if (linkedScopes.length) { + var newWatchScope = linkedScopes[0]; + angular.forEach(expToData, function(data) { + if (data.scope === removedScope) { + data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn); + data.scope = newWatchScope; + } + }); + } else { + expToData = {}; } - - return targetElPos; } - }; - }]); + } + }; +}]); +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass']) + +.value('$datepickerSuppressError', false) -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) +.value('$datepickerLiteralWarning', true) -.constant('datepickerConfig', { +.constant('uibDatepickerConfig', { + datepickerMode: 'day', formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', + maxDate: null, maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, minDate: null, - maxDate: null, - shortcutPropagation: false + minMode: 'day', + monthColumns: 3, + ngModelOptions: {}, + shortcutPropagation: false, + showWeeks: true, + yearColumns: 5, + yearRows: 4 }) -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { +.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', + function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; + ngModelOptions = {}, + watchListeners = []; + + $element.addClass('uib-datepicker'); + $attrs.$set('role', 'application'); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } // Modes chain this.modes = ['day', 'month', 'year']; - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); + [ + 'customClass', + 'dateDisabled', + 'datepickerMode', + 'formatDay', + 'formatDayHeader', + 'formatDayTitle', + 'formatMonth', + 'formatMonthTitle', + 'formatYear', + 'maxDate', + 'maxMode', + 'minDate', + 'minMode', + 'monthColumns', + 'showWeeks', + 'shortcutPropagation', + 'startingDay', + 'yearColumns', + 'yearRows' + ].forEach(function(key) { + switch (key) { + case 'customClass': + case 'dateDisabled': + $scope[key] = $scope.datepickerOptions[key] || angular.noop; + break; + case 'datepickerMode': + $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ? + $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode; + break; + case 'formatDay': + case 'formatDayHeader': + case 'formatDayTitle': + case 'formatMonth': + case 'formatMonthTitle': + case 'formatYear': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $interpolate($scope.datepickerOptions[key])($scope.$parent) : + datepickerConfig[key]; + break; + case 'monthColumns': + case 'showWeeks': + case 'shortcutPropagation': + case 'yearColumns': + case 'yearRows': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $scope.datepickerOptions[key] : datepickerConfig[key]; + break; + case 'startingDay': + if (angular.isDefined($scope.datepickerOptions.startingDay)) { + self.startingDay = $scope.datepickerOptions.startingDay; + } else if (angular.isNumber(datepickerConfig.startingDay)) { + self.startingDay = datepickerConfig.startingDay; + } else { + self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7; + } - // Watchable date attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + break; + case 'maxDate': + case 'minDate': + $scope.$watch('datepickerOptions.' + key, function(value) { + if (value) { + if (angular.isDate(value)) { + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone')); + } else { + if ($datepickerLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + self[key] = new Date(dateFilter(value, 'medium')); + } + } else { + self[key] = datepickerConfig[key] ? + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) : + null; + } + + self.refreshView(); + }); + + break; + case 'maxMode': + case 'minMode': + if ($scope.datepickerOptions[key]) { + $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) { + self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key]; + if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) || + key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) { + $scope.datepickerMode = self[key]; + $scope.datepickerOptions.datepickerMode = self[key]; + } + }); + } else { + self[key] = $scope[key] = datepickerConfig[key] || null; + } + + break; } }); - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; - $scope.maxMode = self.maxMode; $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - if(angular.isDefined($attrs.initDate)) { - this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date(); - $scope.$parent.$watch($attrs.initDate, function(initDate){ - if(initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)){ - self.activeDate = initDate; - self.refreshView(); - } - }); - } else { - this.activeDate = new Date(); + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if (angular.isDefined($attrs.ngDisabled)) { + watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) { + $scope.disabled = disabled; + self.refreshView(); + })); } $scope.isActive = function(dateObject) { @@ -1077,8 +1589,26 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return false; }; - this.init = function( ngModelCtrl_ ) { + this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; + ngModelOptions = extractOptions(ngModelCtrl); + + if ($scope.datepickerOptions.initDate) { + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); + $scope.$watch('datepickerOptions.initDate', function(initDate) { + if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); + self.refreshView(); + } + }); + } else { + self.activeDate = new Date(); + } + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); + this.activeDate = !isNaN(date) ? + dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : + dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); @@ -1086,48 +1616,72 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }; this.render = function() { - if ( ngModelCtrl.$viewValue ) { - var date = new Date( ngModelCtrl.$viewValue ), + if (ngModelCtrl.$viewValue) { + var date = new Date(ngModelCtrl.$viewValue), isValid = !isNaN(date); - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + if (isValid) { + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); + } else if (!$datepickerSuppressError) { + $log.error('Datepicker directive: "ng-model" value must be a Date object'); } - ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { - if ( this.element ) { + if (this.element) { + $scope.selectedDt = null; this._refreshView(); + if ($scope.activeDt) { + $scope.activeDateId = $scope.activeDt.uid; + } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); + ngModelCtrl.$setValidity('dateDisabled', !date || + this.element && !this.isDisabled(date)); } }; this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - return { + model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); + var today = new Date(); + today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); + var time = this.compare(date, today); + var dt = { date: date, - label: dateFilter(date, format), + label: dateParser.filter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0, - customClass: this.customClass(date) + past: time < 0, + current: time === 0, + future: time > 0, + customClass: this.customClass(date) || null }; + + if (model && this.compare(date, model) === 0) { + $scope.selectedDt = dt; + } + + if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) { + $scope.activeDt = dt; + } + + return dt; }; - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + this.isDisabled = function(date) { + return $scope.disabled || + this.minDate && this.compare(date, this.minDate) < 0 || + this.maxDate && this.compare(date, this.maxDate) > 0 || + $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}); }; - this.customClass = function( date ) { - return $scope.customClass({date: date, mode: $scope.datepickerMode}); - }; + this.customClass = function(date) { + return $scope.customClass({date: date, mode: $scope.datepickerMode}); + }; // Split array into smaller arrays this.split = function(arr, size) { @@ -1138,667 +1692,1702 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return arrays; }; - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); + $scope.select = function(date) { + if ($scope.datepickerMode === self.minMode) { + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); + ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]); + + $scope.$emit('uib:datepicker.mode'); } + + $scope.$broadcast('uib:datepicker.focus'); }; - $scope.move = function( direction ) { + $scope.move = function(direction) { var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), month = self.activeDate.getMonth() + direction * (self.step.months || 0); self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; - $scope.toggleMode = function( direction ) { + $scope.toggleMode = function(direction) { direction = direction || 1; - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + if ($scope.datepickerMode === self.maxMode && direction === 1 || + $scope.datepickerMode === self.minMode && direction === -1) { return; } - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]); + + $scope.$emit('uib:datepicker.mode'); }; // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); + self.element[0].focus(); }; // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); + $scope.$on('uib:datepicker.focus', focusElement); - $scope.keydown = function( evt ) { + $scope.keydown = function(evt) { var key = $scope.keys[evt.which]; - if ( !key || evt.shiftKey || evt.altKey ) { + if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { return; } evt.preventDefault(); - if(!self.shortcutPropagation){ - evt.stopPropagation(); + if (!self.shortcutPropagation) { + evt.stopPropagation(); } if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { + if (self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); - focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; -}]) -.directive( 'datepicker', function () { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&', - customClass: '&', - shortcutPropagation: '&?' - }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', - link: function(scope, element, attrs, ctrls) { - var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + $element.on('keydown', function(evt) { + $scope.$apply(function() { + $scope.keydown(evt); + }); + }); - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } + $scope.$on('$destroy', function() { + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); } - }; -}) + }); -.directive('daypicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; + function setMode(mode) { + $scope.datepickerMode = mode; + $scope.datepickerOptions.datepickerMode = mode; + } - ctrl.step = { months: 1 }; - ctrl.element = element; + function extractOptions(ngModelCtrl) { + var ngModelOptions; - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || + $scope.datepickerOptions.ngModelOptions || + datepickerConfig.ngModelOptions || + {}; - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value + var timezone = ngModelCtrl.$options.getOption('timezone') || + ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || + (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); + + // values passed to createChild override existing values + ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance + .createChild(datepickerConfig.ngModelOptions) // lowest precedence + .createChild($scope.datepickerOptions.ngModelOptions) + .createChild(ngModelCtrl.$options) // highest precedence + .createChild({timezone: timezone}); // to keep from squashing a non-null value + } - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); + return ngModelOptions; + } +}]) - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } +.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } + this.step = { months: 1 }; + this.element = $element; + function getDaysInMonth(year, month) { + return month === 1 && year % 4 === 0 && + (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month]; + } - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } + this.init = function(ctrl) { + angular.extend(ctrl, this); + scope.showWeeks = ctrl.showWeeks; + ctrl.refreshView(); + }; - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); + this.getDates = function(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0, date; + while (i < n) { + date = new Date(current); + dates[i++] = date; + current.setDate(current.getDate() + 1); + } + return dates; + }; - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7, - numWeeks = scope.rows.length; - for (var curWeek = 0; curWeek < numWeeks; curWeek++) { - scope.weekNumbers.push( - getISO8601WeekNumber( scope.rows[curWeek][thursdayIndex].date )); - } - } - }; + this._refreshView = function() { + var year = this.activeDate.getFullYear(), + month = this.activeDate.getMonth(), + firstDayOfMonth = new Date(this.activeDate); - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; + firstDayOfMonth.setFullYear(year, month, 1); + + var difference = this.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = difference > 0 ? + 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if (numDisplayedFromPreviousMonth > 0) { + firstDate.setDate(-numDisplayedFromPreviousMonth + 1); + } - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); + // 42 is the number of days on a six-week calendar + var days = this.getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, this.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') }; + } - ctrl.refreshView(); + scope.title = dateFilter(this.activeDate, this.formatDayTitle); + scope.rows = this.split(days, 7); + + if (scope.showWeeks) { + scope.weekNumbers = []; + var thursdayIndex = (4 + 7 - this.startingDay) % 7, + numWeeks = scope.rows.length; + for (var curWeek = 0; curWeek < numWeeks; curWeek++) { + scope.weekNumbers.push( + getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); + } + } + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 7; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()); } + this.activeDate.setDate(date); }; }]) -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } +.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + this.step = { years: 1 }; + this.element = $element; - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; + this.init = function(ctrl) { + angular.extend(ctrl, this); + ctrl.refreshView(); + }; - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; + this._refreshView = function() { + var months = new Array(12), + year = this.activeDate.getFullYear(), + date; - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; + for (var i = 0; i < 12; i++) { + date = new Date(this.activeDate); + date.setFullYear(year, i, 1); + months[i] = angular.extend(this.createDateObject(date, this.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } - ctrl.refreshView(); + scope.title = dateFilter(this.activeDate, this.formatMonthTitle); + scope.rows = this.split(months, this.monthColumns); + scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1; + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - this.monthColumns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + this.monthColumns; + } else if (key === 'pageup' || key === 'pagedown') { + var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; } + this.activeDate.setMonth(date); }; }]) -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; +.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var columns, range; + this.element = $element; - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } + function getStartingYear(year) { + return parseInt((year - 1) / range, 10) * range + 1; + } - ctrl._refreshView = function() { - var years = new Array(range); + this.yearpickerInit = function() { + columns = this.yearColumns; + range = this.yearRows * columns; + this.step = { years: range }; + }; - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } + this._refreshView = function() { + var years = new Array(range), date; - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; + for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) { + date = new Date(this.activeDate); + date.setFullYear(start + i, 0, 1); + years[i] = angular.extend(this.createDateObject(date, this.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = this.split(years, columns); + scope.columns = columns; + }; - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; + this.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; - ctrl.refreshView(); + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - columns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + columns; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * range; + } else if (key === 'home') { + date = getStartingYear(this.activeDate.getFullYear()); + } else if (key === 'end') { + date = getStartingYear(this.activeDate.getFullYear()) + range - 1; } + this.activeDate.setFullYear(date); }; }]) -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - html5Types: { - date: 'yyyy-MM-dd', - 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', - 'month': 'yyyy-MM' - }, - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { +.directive('uibDatepicker', function() { return { - restrict: 'EA', - require: 'ngModel', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; + }, scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&', - customClass: '&' + datepickerOptions: '=?' }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; - - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + require: ['uibDatepicker', '^ngModel'], + restrict: 'A', + controller: 'UibDatepickerController', + controllerAs: 'datepicker', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; + datepickerCtrl.init(ngModelCtrl); + } + }; +}) - var isHtml5DateInput = false; - if (datepickerPopupConfig.html5Types[attrs.type]) { - dateFormat = datepickerPopupConfig.html5Types[attrs.type]; - isHtml5DateInput = true; - } else { - dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; - attrs.$observe('datepickerPopup', function(value, oldValue) { - var newDateFormat = value || datepickerPopupConfig.datepickerPopup; - // Invalidate the $modelValue to ensure that formatters re-run - // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 - if (newDateFormat !== dateFormat) { - dateFormat = newDateFormat; - ngModel.$modelValue = null; - - if (!dateFormat) { - throw new Error('datepickerPopup must have a date format specified.'); - } - } - }); - } +.directive('uibDaypicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/day.html'; + }, + require: ['^uibDatepicker', 'uibDaypicker'], + restrict: 'A', + controller: 'UibDaypickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + daypickerCtrl = ctrls[1]; - if (!dateFormat) { - throw new Error('datepickerPopup must have a date format specified.'); - } + daypickerCtrl.init(datepickerCtrl); + } + }; +}) - if (isHtml5DateInput && attrs.datepickerPopup) { - throw new Error('HTML5 date input types do not support custom formats.'); - } +.directive('uibMonthpicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/month.html'; + }, + require: ['^uibDatepicker', 'uibMonthpicker'], + restrict: 'A', + controller: 'UibMonthpickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + monthpickerCtrl = ctrls[1]; - // popup element used to display calendar - var popupEl = angular.element('
'); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); + monthpickerCtrl.init(datepickerCtrl); + } + }; +}) - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } +.directive('uibYearpicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/year.html'; + }, + require: ['^uibDatepicker', 'uibYearpicker'], + restrict: 'A', + controller: 'UibYearpickerController', + link: function(scope, element, attrs, ctrls) { + var ctrl = ctrls[0]; + angular.extend(ctrl, ctrls[1]); + ctrl.yearpickerInit(); - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if (isHtml5DateInput) { - if (attrs.type == 'month') { - datepickerEl.attr('datepicker-mode', '"month"'); - datepickerEl.attr('min-mode', 'month'); - } - } + ctrl.refreshView(); + } + }; +}); - if ( attrs.datepickerOptions ) { - var options = scope.$parent.$eval(attrs.datepickerOptions); - if(options.initDate) { - scope.initDate = options.initDate; - datepickerEl.attr( 'init-date', 'initDate' ); - delete options.initDate; - } - angular.forEach(options, function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); - }); - } +angular.module('ui.bootstrap.position', []) - scope.watchData = {}; - angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) { - if ( attrs[key] ) { - var getAttribute = $parse(attrs[key]); - scope.$parent.$watch(getAttribute, function(value){ - scope.watchData[key] = value; - }); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if ( key === 'datepickerMode' ) { - var setAttribute = getAttribute.assign; - scope.$watch('watchData.' + key, function(value, oldvalue) { - if ( angular.isFunction(setAttribute) && value !== oldvalue ) { - setAttribute(scope.$parent, value); - } - }); - } - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } +/** + * A set of utility methods for working with the DOM. + * It is meant to be used where we need to absolute-position elements in + * relation to another element (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$uibPosition', ['$document', '$window', function($document, $window) { + /** + * Used by scrollbarWidth() function to cache scrollbar's width. + * Do not access this variable directly, use scrollbarWidth() instead. + */ + var SCROLLBAR_WIDTH; + /** + * scrollbar on body and html element in IE and Edge overlay + * content and should be considered 0 width. + */ + var BODY_SCROLLBAR_WIDTH; + var OVERFLOW_REGEX = { + normal: /(auto|scroll)/, + hidden: /(auto|scroll|hidden)/ + }; + var PLACEMENT_REGEX = { + auto: /\s?auto?\s?/i, + primary: /^(top|bottom|left|right)$/, + secondary: /^(top|bottom|left|right|center)$/, + vertical: /^(top|bottom)$/ + }; + var BODY_REGEX = /(HTML|BODY)/; - if (attrs.showWeeks) { - datepickerEl.attr('show-weeks', attrs.showWeeks); - } + return { - if (attrs.customClass){ - datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); - } + /** + * Provides a raw DOM element from a jQuery/jQLite element. + * + * @param {element} elem - The element to convert. + * + * @returns {element} A HTML element. + */ + getRawNode: function(elem) { + return elem.nodeName ? elem : elem[0] || elem; + }, - function parseDate(viewValue) { - if (angular.isNumber(viewValue)) { - // presumably timestamp to date object - viewValue = new Date(viewValue); - } + /** + * Provides a parsed number for a style property. Strips + * units and casts invalid numbers to 0. + * + * @param {string} value - The style value to parse. + * + * @returns {number} A valid number. + */ + parseStyle: function(value) { + value = parseFloat(value); + return isFinite(value) ? value : 0; + }, + + /** + * Provides the closest positioned ancestor. + * + * @param {element} element - The element to get the offest parent for. + * + * @returns {element} The closest positioned ancestor. + */ + offsetParent: function(elem) { + elem = this.getRawNode(elem); + + var offsetParent = elem.offsetParent || $document[0].documentElement; + + function isStaticPositioned(el) { + return ($window.getComputedStyle(el).position || 'static') === 'static'; + } + + while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + + return offsetParent || $document[0].documentElement; + }, + + /** + * Provides the scrollbar width, concept from TWBS measureScrollbar() + * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js + * In IE and Edge, scollbar on body and html element overlay and should + * return a width of 0. + * + * @returns {number} The width of the browser scollbar. + */ + scrollbarWidth: function(isBody) { + if (isBody) { + if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { + var bodyElem = $document.find('body'); + bodyElem.addClass('uib-position-body-scrollbar-measure'); + BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; + BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; + bodyElem.removeClass('uib-position-body-scrollbar-measure'); + } + return BODY_SCROLLBAR_WIDTH; + } + + if (angular.isUndefined(SCROLLBAR_WIDTH)) { + var scrollElem = angular.element('
'); + $document.find('body').append(scrollElem); + SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; + SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; + scrollElem.remove(); + } + + return SCROLLBAR_WIDTH; + }, + + /** + * Provides the padding required on an element to replace the scrollbar. + * + * @returns {object} An object with the following properties: + * + */ + scrollbarPadding: function(elem) { + elem = this.getRawNode(elem); + + var elemStyle = $window.getComputedStyle(elem); + var paddingRight = this.parseStyle(elemStyle.paddingRight); + var paddingBottom = this.parseStyle(elemStyle.paddingBottom); + var scrollParent = this.scrollParent(elem, false, true); + var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName)); + + return { + scrollbarWidth: scrollbarWidth, + widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, + right: paddingRight + scrollbarWidth, + originalRight: paddingRight, + heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, + bottom: paddingBottom + scrollbarWidth, + originalBottom: paddingBottom + }; + }, + + /** + * Checks to see if the element is scrollable. + * + * @param {element} elem - The element to check. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * + * @returns {boolean} Whether the element is scrollable. + */ + isScrollable: function(elem, includeHidden) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var elemStyle = $window.getComputedStyle(elem); + return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); + }, + + /** + * Provides the closest scrollable ancestor. + * A port of the jQuery UI scrollParent method: + * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js + * + * @param {element} elem - The element to find the scroll parent of. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * @param {boolean=} [includeSelf=false] - Should the element being passed be + * included in the scrollable llokup. + * + * @returns {element} A HTML element. + */ + scrollParent: function(elem, includeHidden, includeSelf) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var documentEl = $document[0].documentElement; + var elemStyle = $window.getComputedStyle(elem); + if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { + return elem; + } + var excludeStatic = elemStyle.position === 'absolute'; + var scrollParent = elem.parentElement || documentEl; + + if (scrollParent === documentEl || elemStyle.position === 'fixed') { + return documentEl; + } + + while (scrollParent.parentElement && scrollParent !== documentEl) { + var spStyle = $window.getComputedStyle(scrollParent); + if (excludeStatic && spStyle.position !== 'static') { + excludeStatic = false; + } + + if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { + break; + } + scrollParent = scrollParent.parentElement; + } + + return scrollParent; + }, + + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ - distance to closest positioned + * ancestor. Does not account for margins by default like jQuery position. + * + * @param {element} elem - The element to caclulate the position on. + * @param {boolean=} [includeMargins=false] - Should margins be accounted + * for, default is false. + * + * @returns {object} An object with the following properties: + * + */ + position: function(elem, includeMagins) { + elem = this.getRawNode(elem); + + var elemOffset = this.offset(elem); + if (includeMagins) { + var elemStyle = $window.getComputedStyle(elem); + elemOffset.top -= this.parseStyle(elemStyle.marginTop); + elemOffset.left -= this.parseStyle(elemStyle.marginLeft); + } + var parent = this.offsetParent(elem); + var parentOffset = {top: 0, left: 0}; + + if (parent !== $document[0].documentElement) { + parentOffset = this.offset(parent); + parentOffset.top += parent.clientTop - parent.scrollTop; + parentOffset.left += parent.clientLeft - parent.scrollLeft; + } + + return { + width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), + top: Math.round(elemOffset.top - parentOffset.top), + left: Math.round(elemOffset.left - parentOffset.left) + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ - distance to viewport. Does + * not account for borders, margins, or padding on the body + * element. + * + * @param {element} elem - The element to calculate the offset on. + * + * @returns {object} An object with the following properties: + * + */ + offset: function(elem) { + elem = this.getRawNode(elem); + + var elemBCR = elem.getBoundingClientRect(); + return { + width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), + top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), + left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) + }; + }, + + /** + * Provides offset distance to the closest scrollable ancestor + * or viewport. Accounts for border and scrollbar width. + * + * Right and bottom dimensions represent the distance to the + * respective edge of the viewport element. If the element + * edge extends beyond the viewport, a negative value will be + * reported. + * + * @param {element} elem - The element to get the viewport offset for. + * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead + * of the first scrollable element, default is false. + * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element + * be accounted for, default is true. + * + * @returns {object} An object with the following properties: + * + */ + viewportOffset: function(elem, useDocument, includePadding) { + elem = this.getRawNode(elem); + includePadding = includePadding !== false ? true : false; + + var elemBCR = elem.getBoundingClientRect(); + var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; + + var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); + var offsetParentBCR = offsetParent.getBoundingClientRect(); + + offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; + offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; + if (offsetParent === $document[0].documentElement) { + offsetBCR.top += $window.pageYOffset; + offsetBCR.left += $window.pageXOffset; + } + offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; + offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; + + if (includePadding) { + var offsetParentStyle = $window.getComputedStyle(offsetParent); + offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); + offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); + offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); + offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); + } + + return { + top: Math.round(elemBCR.top - offsetBCR.top), + bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), + left: Math.round(elemBCR.left - offsetBCR.left), + right: Math.round(offsetBCR.right - elemBCR.right) + }; + }, + + /** + * Provides an array of placement values parsed from a placement string. + * Along with the 'auto' indicator, supported placement strings are: + * + * A placement string with an 'auto' indicator is expected to be + * space separated from the placement, i.e: 'auto bottom-left' If + * the primary and secondary placement values do not match 'top, + * bottom, left, right' then 'top' will be the primary placement and + * 'center' will be the secondary placement. If 'auto' is passed, true + * will be returned as the 3rd value of the array. + * + * @param {string} placement - The placement string to parse. + * + * @returns {array} An array with the following values + * + */ + parsePlacement: function(placement) { + var autoPlace = PLACEMENT_REGEX.auto.test(placement); + if (autoPlace) { + placement = placement.replace(PLACEMENT_REGEX.auto, ''); + } + + placement = placement.split('-'); + + placement[0] = placement[0] || 'top'; + if (!PLACEMENT_REGEX.primary.test(placement[0])) { + placement[0] = 'top'; + } + + placement[1] = placement[1] || 'center'; + if (!PLACEMENT_REGEX.secondary.test(placement[1])) { + placement[1] = 'center'; + } + + if (autoPlace) { + placement[2] = true; + } else { + placement[2] = false; + } + + return placement; + }, + + /** + * Provides coordinates for an element to be positioned relative to + * another element. Passing 'auto' as part of the placement parameter + * will enable smart placement - where the element fits. i.e: + * 'auto left-top' will check to see if there is enough space to the left + * of the hostElem to fit the targetElem, if not place right (same for secondary + * top placement). Available space is calculated using the viewportOffset + * function. + * + * @param {element} hostElem - The element to position against. + * @param {element} targetElem - The element to position. + * @param {string=} [placement=top] - The placement for the targetElem, + * default is 'top'. 'center' is assumed as secondary placement for + * 'top', 'left', 'right', and 'bottom' placements. Available placements are: + * + * @param {boolean=} [appendToBody=false] - Should the top and left values returned + * be calculated from the body element, default is false. + * + * @returns {object} An object with the following properties: + * + */ + positionElements: function(hostElem, targetElem, placement, appendToBody) { + hostElem = this.getRawNode(hostElem); + targetElem = this.getRawNode(targetElem); + + // need to read from prop to support tests. + var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); + var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); + + placement = this.parsePlacement(placement); + + var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); + var targetElemPos = {top: 0, left: 0, placement: ''}; + + if (placement[2]) { + var viewportOffset = this.viewportOffset(hostElem, appendToBody); + + var targetElemStyle = $window.getComputedStyle(targetElem); + var adjustedSize = { + width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), + height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) + }; + + placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : + placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : + placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : + placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : + placement[0]; + + placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : + placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : + placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : + placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : + placement[1]; + + if (placement[1] === 'center') { + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + var xOverflow = hostElemPos.width / 2 - targetWidth / 2; + if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { + placement[1] = 'left'; + } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { + placement[1] = 'right'; + } + } else { + var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; + if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { + placement[1] = 'top'; + } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { + placement[1] = 'bottom'; + } + } + } + } + + switch (placement[0]) { + case 'top': + targetElemPos.top = hostElemPos.top - targetHeight; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height; + break; + case 'left': + targetElemPos.left = hostElemPos.left - targetWidth; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width; + break; + } + + switch (placement[1]) { + case 'top': + targetElemPos.top = hostElemPos.top; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; + break; + case 'left': + targetElemPos.left = hostElemPos.left; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; + break; + case 'center': + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; + } else { + targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; + } + break; + } + + targetElemPos.top = Math.round(targetElemPos.top); + targetElemPos.left = Math.round(targetElemPos.left); + targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; + + return targetElemPos; + }, + + /** + * Provides a way to adjust the top positioning after first + * render to correctly align element to top after content + * rendering causes resized element height + * + * @param {array} placementClasses - The array of strings of classes + * element should have. + * @param {object} containerPosition - The object with container + * position information + * @param {number} initialHeight - The initial height for the elem. + * @param {number} currentHeight - The current height for the elem. + */ + adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) { + if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) { + return { + top: containerPosition.top - currentHeight + 'px' + }; + } + }, + + /** + * Provides a way for positioning tooltip & dropdown + * arrows when using placement options beyond the standard + * left, right, top, or bottom. + * + * @param {element} elem - The tooltip/dropdown element. + * @param {string} placement - The placement for the elem. + */ + positionArrow: function(elem, placement) { + elem = this.getRawNode(elem); + + var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); + if (!innerElem) { + return; + } + + var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); + + var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); + if (!arrowElem) { + return; + } + + var arrowCss = { + top: '', + bottom: '', + left: '', + right: '' + }; + + placement = this.parsePlacement(placement); + if (placement[1] === 'center') { + // no adjustment necessary - just reset styles + angular.element(arrowElem).css(arrowCss); + return; + } + + var borderProp = 'border-' + placement[0] + '-width'; + var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; + + var borderRadiusProp = 'border-'; + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + borderRadiusProp += placement[0] + '-' + placement[1]; + } else { + borderRadiusProp += placement[1] + '-' + placement[0]; + } + borderRadiusProp += '-radius'; + var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; + + switch (placement[0]) { + case 'top': + arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; + break; + case 'bottom': + arrowCss.top = isTooltip ? '0' : '-' + borderWidth; + break; + case 'left': + arrowCss.right = isTooltip ? '0' : '-' + borderWidth; + break; + case 'right': + arrowCss.left = isTooltip ? '0' : '-' + borderWidth; + break; + } + + arrowCss[placement[1]] = borderRadius; + + angular.element(arrowElem).css(arrowCss); + } + }; + }]); + +angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) + +.value('$datepickerPopupLiteralWarning', true) + +.constant('uibDatepickerPopupConfig', { + altInputFormats: [], + appendToBody: false, + clearText: 'Clear', + closeOnDateSelection: true, + closeText: 'Done', + currentText: 'Today', + datepickerPopup: 'yyyy-MM-dd', + datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html', + datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html', + html5Types: { + date: 'yyyy-MM-dd', + 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', + 'month': 'yyyy-MM' + }, + onOpenFocus: true, + showButtonBar: true, + placement: 'auto bottom-left' +}) + +.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning', +function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = extractOptions(ngModel); + closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? + $scope.$parent.$eval($attrs.closeOnDateSelection) : + datepickerPopupConfig.closeOnDateSelection; + appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ? + $scope.$parent.$eval($attrs.datepickerAppendToBody) : + datepickerPopupConfig.appendToBody; + onOpenFocus = angular.isDefined($attrs.onOpenFocus) ? + $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus; + datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ? + $attrs.datepickerPopupTemplateUrl : + datepickerPopupConfig.datepickerPopupTemplateUrl; + datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ? + $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl; + altInputFormats = angular.isDefined($attrs.altInputFormats) ? + $scope.$parent.$eval($attrs.altInputFormats) : + datepickerPopupConfig.altInputFormats; + + $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ? + $scope.$parent.$eval($attrs.showButtonBar) : + datepickerPopupConfig.showButtonBar; + + if (datepickerPopupConfig.html5Types[$attrs.type]) { + dateFormat = datepickerPopupConfig.html5Types[$attrs.type]; + isHtml5DateInput = true; + } else { + dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; + $attrs.$observe('uibDatepickerPopup', function(value, oldValue) { + var newDateFormat = value || datepickerPopupConfig.datepickerPopup; + // Invalidate the $modelValue to ensure that formatters re-run + // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 + if (newDateFormat !== dateFormat) { + dateFormat = newDateFormat; + ngModel.$modelValue = null; + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + } + }); + } + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + + if (isHtml5DateInput && $attrs.uibDatepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } + + // popup element used to display calendar + popupEl = angular.element('
'); + + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); + + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } + + if (isHtml5DateInput) { + if ($attrs.type === 'month') { + $scope.datepickerOptions.datepickerMode = 'month'; + $scope.datepickerOptions.minMode = 'month'; + } + } + + datepickerEl.attr('datepicker-options', 'datepickerOptions'); + + if (!isHtml5DateInput) { + // Internal API to maintain the correct ng-invalid-[key] class + ngModel.$$parserName = 'date'; + ngModel.$validators.date = validator; + ngModel.$parsers.unshift(parseDate); + ngModel.$formatters.push(function(value) { + if (ngModel.$isEmpty(value)) { + $scope.date = value; + return value; + } + + if (angular.isNumber(value)) { + value = new Date(value); + } + + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); + + return dateParser.filter($scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); + return value; + }); + } + + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + $scope.date = parseDateString(ngModel.$viewValue); + }); + + $element.on('keydown', inputKeydownBind); + + $popup = $compile(popupEl)($scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if (appendToBody) { + $document.find('body').append($popup); + } else { + $element.after($popup); + } + + $scope.$on('$destroy', function() { + if ($scope.isOpen === true) { + if (!$rootScope.$$phase) { + $scope.$apply(function() { + $scope.isOpen = false; + }); + } + } + + $popup.remove(); + $element.off('keydown', inputKeydownBind); + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + $scope.isDisabled = function(date) { + if (date === 'today') { + date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); + } + + var dates = {}; + angular.forEach(['minDate', 'maxDate'], function(key) { + if (!$scope.datepickerOptions[key]) { + dates[key] = null; + } else if (angular.isDate($scope.datepickerOptions[key])) { + dates[key] = new Date($scope.datepickerOptions[key]); + } else { + if ($datepickerPopupLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium')); + } + }); + + return $scope.datepickerOptions && + dates.minDate && $scope.compare(date, dates.minDate) < 0 || + dates.maxDate && $scope.compare(date, dates.maxDate) > 0; + }; + + $scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; + + // Inner change + $scope.dateSelection = function(dt) { + $scope.date = dt; + var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + $element.val(date); + ngModel.$setViewValue(date); + + if (closeOnDateSelection) { + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.keydown = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.select = function(date, evt) { + evt.stopPropagation(); + + if (date === 'today') { + var today = new Date(); + if (angular.isDate($scope.date)) { + date = new Date($scope.date); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); + date.setHours(0, 0, 0, 0); + } + } + $scope.dateSelection(date); + }; - if (!viewValue) { - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue); - if (isNaN(date)) { - return undefined; + $scope.close = function(evt) { + evt.stopPropagation(); + + $scope.isOpen = false; + $element[0].focus(); + }; + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if ($attrs.ngDisabled) { + watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) { + $scope.disabled = disabled; + })); + } + + $scope.$watch('isOpen', function(value) { + if (value) { + if (!$scope.disabled) { + $timeout(function() { + positionPopup(); + + if (onOpenFocus) { + $scope.$broadcast('uib:datepicker.focus'); + } + + $document.on('click', documentClickBind); + + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + if (appendToBody || $position.parsePlacement(placement)[2]) { + scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element)); + if (scrollParentEl) { + scrollParentEl.on('scroll', positionPopup); + } } else { - return date; + scrollParentEl = null; } - } else { - return undefined; - } + + angular.element($window).on('resize', positionPopup); + }, 0, false); + } else { + $scope.isOpen = false; + } + } else { + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); } + angular.element($window).off('resize', positionPopup); + } + }); - function validator(modelValue, viewValue) { - var value = modelValue || viewValue; - if (angular.isNumber(value)) { - value = new Date(value); - } - if (!value) { - return true; - } else if (angular.isDate(value) && !isNaN(value)) { - return true; - } else if (angular.isString(value)) { - var date = dateParser.parse(value, dateFormat) || new Date(value); - return !isNaN(date); - } else { - return false; + function cameltoDash(string) { + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + function parseDateString(viewValue) { + var date = dateParser.parse(viewValue, dateFormat, $scope.date); + if (isNaN(date)) { + for (var i = 0; i < altInputFormats.length; i++) { + date = dateParser.parse(viewValue, altInputFormats[i], $scope.date); + if (!isNaN(date)) { + return date; } } + } + return date; + } - if (!isHtml5DateInput) { - // Internal API to maintain the correct ng-invalid-[key] class - ngModel.$$parserName = 'date'; - ngModel.$validators.date = validator; - ngModel.$parsers.unshift(parseDate); - ngModel.$formatters.push(function (value) { - scope.date = value; - return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); - }); - } - else { - ngModel.$formatters.push(function (value) { - scope.date = value; - return value; - }); + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } + + if (!viewValue) { + return null; + } + + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } + + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } + } - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - var date = scope.date ? dateFilter(scope.date, dateFormat) : ''; - element.val(date); - ngModel.$setViewValue(date); + return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; + } - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); - } - }; + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; - // Detect changes in the view from the text box - ngModel.$viewChangeListeners.push(function () { - scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue); - }); + if (!$attrs.ngRequired && !value) { + return true; + } - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - }; + if (angular.isNumber(value)) { + value = new Date(value); + } - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); + if (!value) { + return true; + } - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - if (scope.isOpen) { - evt.stopPropagation(); - } - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; + if (angular.isDate(value) && !isNaN(value)) { + return true; + } - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); + if (angular.isString(value)) { + return !isNaN(parseDateString(value)); + } - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); - } + return false; + } + + function documentClickBind(event) { + if (!$scope.isOpen && $scope.disabled) { + return; + } + + var popup = $popup[0]; + var dpContainsTarget = $element[0].contains(event.target); + // The popup node may not be an element node + // In some browsers (IE) only element nodes have the 'contains' function + var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); + if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { + $scope.$apply(function() { + $scope.isOpen = false; }); + } + } - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(scope.date)) { - date = new Date(scope.date); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); - } else { - date = new Date(today.setHours(0, 0, 0, 0)); - } - } - scope.dateSelection( date ); - }; + function inputKeydownBind(evt) { + if (evt.which === 27 && $scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = false; + }); + $element[0].focus(); + } else if (evt.which === 40 && !$scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = true; + }); + } + } + + function positionPopup() { + if ($scope.isOpen) { + var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup')); + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + var position = $position.positionElements($element, dpElement, placement, appendToBody); + dpElement.css({top: position.top + 'px', left: position.left + 'px'}); + if (dpElement.hasClass('uib-position-measure')) { + dpElement.removeClass('uib-position-measure'); + } + } + } + + function extractOptions(ngModelCtrl) { + var ngModelOptions; - scope.close = function() { - scope.isOpen = false; - element[0].focus(); + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = angular.isObject(ngModelCtrl.$options) ? + ngModelCtrl.$options : + { + timezone: null + }; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } - var $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); + $scope.$on('uib:datepicker.mode', function() { + $timeout(positionPopup, 0, false); + }); +}]) - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } +.directive('uibDatepickerPopup', function() { + return { + require: ['ngModel', 'uibDatepickerPopup'], + controller: 'UibDatepickerPopupController', + scope: { + datepickerOptions: '=?', + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@' + }, + link: function(scope, element, attrs, ctrls) { + var ngModel = ctrls[0], + ctrl = ctrls[1]; - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); - }); + ctrl.init(ngModel); } }; -}]) +}) -.directive('datepickerPopupWrap', function() { +.directive('uibDatepickerPopupWrap', function() { return { - restrict:'EA', - replace: true, + restrict: 'A', transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; } }; }); -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.debounce', []) +/** + * A helper, internal service that debounces a function + */ + .factory('$$debounce', ['$timeout', function($timeout) { + return function(callback, debounceTime) { + var timeoutPromise; + + return function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + + timeoutPromise = $timeout(function() { + callback.apply(self, args); + }, debounceTime); + }; + }; + }]); + +angular.module('ui.bootstrap.multiMap', []) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }); + +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) -.constant('dropdownConfig', { +.constant('uibDropdownConfig', { + appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) { +.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) { var openScope = null; + var openedContainers = $$multiMap.createNew(); + + this.isOnlyOpen = function(dropdownScope, appendTo) { + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (openDropdown) { + return openedDropdowns.length === 1; + } + } - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', escapeKeyBind); + return false; + }; + + this.open = function(dropdownScope, element, appendTo) { + if (!openScope) { + $document.on('click', closeDropdown); } - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; + if (openScope && openScope !== dropdownScope) { + openScope.isOpen = false; } openScope = dropdownScope; + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openedScopes = openedDropdowns.map(function(dropdown) { + return dropdown.scope; + }); + if (openedScopes.indexOf(dropdownScope) === -1) { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } + } else { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } }; - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { + this.close = function(dropdownScope, element, appendTo) { + if (openScope === dropdownScope) { + $document.off('click', closeDropdown); + $document.off('keydown', this.keybindFilter); openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', escapeKeyBind); + } + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); + } } }; - var closeDropdown = function( evt ) { + var closeDropdown = function(evt) { // This method may still be called during the same mouse event that // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } + if (!openScope || !openScope.isOpen) { return; } + + if (evt && openScope.getAutoClose() === 'disabled') { return; } - if( evt && openScope.getAutoClose() === 'disabled' ) { return ; } + if (evt && evt.which === 3) { return; } var toggleElement = openScope.getToggleElement(); - if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { - return; + if (evt && toggleElement && toggleElement[0].contains(evt.target)) { + return; } - var $element = openScope.getElement(); - if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) { + var dropdownElement = openScope.getDropdownElement(); + if (evt && openScope.getAutoClose() === 'outsideClick' && + dropdownElement && dropdownElement[0].contains(evt.target)) { return; } + openScope.focusToggleElement(); openScope.isOpen = false; if (!$rootScope.$$phase) { @@ -1806,28 +3395,45 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; - var escapeKeyBind = function( evt ) { - if ( evt.which === 27 ) { + this.keybindFilter = function(evt) { + if (!openScope) { + // see this.close as ESC could have been pressed which kills the scope so we can not proceed + return; + } + + var dropdownElement = openScope.getDropdownElement(); + var toggleElement = openScope.getToggleElement(); + var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target); + var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target); + if (evt.which === 27) { + evt.stopPropagation(); openScope.focusToggleElement(); closeDropdown(); + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) { + evt.preventDefault(); + evt.stopPropagation(); + openScope.focusDropdownEntry(evt.which); } }; }]) -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) { +.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - templateScope, - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, - appendToBody = false; - - this.init = function( element ) { - self.$element = element; - - if ( $attrs.isOpen ) { + scope = $scope.$new(), // create a child scope so we are not polluting original one + templateScope, + appendToOpenClass = dropdownConfig.appendToOpenClass, + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + keynavEnabled = false, + selectedOption = null, + body = $document.find('body'); + + $element.addClass('dropdown'); + + this.init = function() { + if ($attrs.isOpen) { getIsOpen = $parse($attrs.isOpen); setIsOpen = getIsOpen.assign; @@ -1836,56 +3442,180 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); } - appendToBody = angular.isDefined($attrs.dropdownAppendToBody); + keynavEnabled = angular.isDefined($attrs.keyboardNav); + }; - if ( appendToBody && self.dropdownMenu ) { - $document.find('body').append( self.dropdownMenu ); - element.on('$destroy', function handleDestroyEvent() { - self.dropdownMenu.remove(); - }); + this.toggle = function(open) { + scope.isOpen = arguments.length ? !!open : !scope.isOpen; + if (angular.isFunction(setIsOpen)) { + setIsOpen(scope, scope.isOpen); + } + + return scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.getAutoClose = function() { + return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' + }; + + scope.getElement = function() { + return $element; + }; + + scope.isKeynavEnabled = function() { + return keynavEnabled; + }; + + scope.focusDropdownEntry = function(keyCode) { + var elems = self.dropdownMenu ? //If append to body is used. + angular.element(self.dropdownMenu).find('a') : + $element.find('ul').eq(0).find('a'); + + switch (keyCode) { + case 40: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = 0; + } else { + self.selectedOption = self.selectedOption === elems.length - 1 ? + self.selectedOption : + self.selectedOption + 1; + } + break; + } + case 38: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = elems.length - 1; + } else { + self.selectedOption = self.selectedOption === 0 ? + 0 : self.selectedOption - 1; + } + break; + } + } + elems[self.selectedOption].focus(); + }; + + scope.getDropdownElement = function() { + return self.dropdownMenu; + }; + + scope.focusToggleElement = function() { + if (self.toggleElement) { + self.toggleElement[0].focus(); } }; - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; - }; + function removeDropdownMenu() { + $element.append(self.dropdownMenu); + } + + scope.$watch('isOpen', function(isOpen, wasOpen) { + var appendTo = null, + appendToBody = false; + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + if (angular.isDefined($attrs.dropdownAppendToBody)) { + var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope); + if (appendToBodyValue !== false) { + appendToBody = true; + } + } + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + if (isOpen) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', removeDropdownMenu); + } else { + $element.off('$destroy', removeDropdownMenu); + removeDropdownMenu(); + } + } + + if (appendTo && self.dropdownMenu) { + var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), + css, + rightalign, + scrollbarPadding, + scrollbarWidth = 0; + + css = { + top: pos.top + 'px', + display: isOpen ? 'block' : 'none' + }; + + rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); + if (!rightalign) { + css.left = pos.left + 'px'; + css.right = 'auto'; + } else { + css.left = 'auto'; + scrollbarPadding = $position.scrollbarPadding(appendTo); + + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + scrollbarWidth = scrollbarPadding.scrollbarWidth; + } - // Allow other directives to watch status - this.isOpen = function() { - return scope.isOpen; - }; + css.right = window.innerWidth - scrollbarWidth - + (pos.left + $element.prop('offsetWidth')) + 'px'; + } - scope.getToggleElement = function() { - return self.toggleElement; - }; + // Need to adjust our positioning to be relative to the appendTo container + // if it's not the body element + if (!appendToBody) { + var appendOffset = $position.offset(appendTo); - scope.getAutoClose = function() { - return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' - }; + css.top = pos.top - appendOffset.top + 'px'; - scope.getElement = function() { - return self.$element; - }; + if (!rightalign) { + css.left = pos.left - appendOffset.left + 'px'; + } else { + css.right = window.innerWidth - + (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; + } + } - scope.focusToggleElement = function() { - if ( self.toggleElement ) { - self.toggleElement[0].focus(); + self.dropdownMenu.css(css); } - }; - scope.$watch('isOpen', function( isOpen, wasOpen ) { - if ( appendToBody && self.dropdownMenu ) { - var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true); - self.dropdownMenu.css({ - top: pos.top + 'px', - left: pos.left + 'px', - display: isOpen ? 'block' : 'none' + var openContainer = appendTo ? appendTo : $element; + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); + + if (hasOpenClass === !isOpen) { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } }); } - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); - - if ( isOpen ) { + if (isOpen) { if (self.dropdownMenuTemplateUrl) { $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { templateScope = scope.$new(); @@ -1893,13 +3623,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) var newEl = dropdownElement; self.dropdownMenu.replaceWith(newEl); self.dropdownMenu = newEl; + $document.on('keydown', uibDropdownService.keybindFilter); }); }); + } else { + $document.on('keydown', uibDropdownService.keybindFilter); } scope.focusToggleElement(); - dropdownService.open( scope ); + uibDropdownService.open(scope, $element, appendTo); } else { + uibDropdownService.close(scope, $element, appendTo); if (self.dropdownMenuTemplateUrl) { if (templateScope) { templateScope.$destroy(); @@ -1909,47 +3643,40 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) self.dropdownMenu = newEl; } - dropdownService.close( scope ); - } - - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); + self.selectedOption = null; } - }); - $scope.$on('$locationChangeSuccess', function() { - if (scope.getAutoClose() !== 'disabled') { - scope.isOpen = false; + if (angular.isFunction(setIsOpen)) { + setIsOpen($scope, isOpen); } }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); }]) -.directive('dropdown', function() { +.directive('uibDropdown', function() { return { - controller: 'DropdownController', + controller: 'UibDropdownController', link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); + dropdownCtrl.init(); } }; }) -.directive('dropdownMenu', function() { +.directive('uibDropdownMenu', function() { return { - restrict: 'AC', - require: '?^dropdown', + restrict: 'A', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if (!dropdownCtrl) { + if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { return; } + + element.addClass('dropdown-menu'); + var tplUrl = attrs.templateUrl; if (tplUrl) { dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; } + if (!dropdownCtrl.dropdownMenu) { dropdownCtrl.dropdownMenu = element; } @@ -1957,62 +3684,63 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }; }) -.directive('dropdownToggle', function() { +.directive('uibDropdownToggle', function() { return { - require: '?^dropdown', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { + if (!dropdownCtrl) { return; } + element.addClass('dropdown-toggle'); + dropdownCtrl.toggleElement = element; var toggleDropdown = function(event) { event.preventDefault(); - if ( !element.hasClass('disabled') && !attrs.disabled ) { + if (!element.hasClass('disabled') && !attrs.disabled) { scope.$apply(function() { dropdownCtrl.toggle(); }); } }; - element.bind('click', toggleDropdown); + element.on('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + scope.$watch(dropdownCtrl.isOpen, function(isOpen) { element.attr('aria-expanded', !!isOpen); }); scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); + element.off('click', toggleDropdown); }); } }; }); -angular.module('ui.bootstrap.modal', []) - +angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ - .factory('$$stackedMap', function () { + .factory('$$stackedMap', function() { return { - createNew: function () { + createNew: function() { var stack = []; return { - add: function (key, value) { + add: function(key, value) { stack.push({ key: key, value: value }); }, - get: function (key) { + get: function(key) { for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { return stack[i]; } } @@ -2024,153 +3752,252 @@ angular.module('ui.bootstrap.modal', []) } return keys; }, - top: function () { + top: function() { return stack[stack.length - 1]; }, - remove: function (key) { + remove: function(key) { var idx = -1; for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, - removeTop: function () { - return stack.splice(stack.length - 1, 1)[0]; + removeTop: function() { + return stack.pop(); }, - length: function () { + length: function() { return stack.length; } }; } }; + }); +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) +/** + * Pluggable resolve mechanism for the modal resolve resolution + * Supports UI Router's $resolve service + */ + .provider('$uibResolve', function() { + var resolve = this; + this.resolver = null; + + this.setResolver = function(resolver) { + this.resolver = resolver; + }; + + this.$get = ['$injector', '$q', function($injector, $q) { + var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; + return { + resolve: function(invocables, locals, parent, self) { + if (resolver) { + return resolver.resolve(invocables, locals, parent, self); + } + + var promises = []; + + angular.forEach(invocables, function(value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promises.push($q.resolve($injector.invoke(value))); + } else if (angular.isString(value)) { + promises.push($q.resolve($injector.get(value))); + } else { + promises.push($q.resolve(value)); + } + }); + + return $q.all(promises).then(function(resolves) { + var resolveObj = {}; + var resolveIter = 0; + angular.forEach(invocables, function(value, key) { + resolveObj[key] = resolves[resolveIter++]; + }); + + return resolveObj; + }); + } + }; + }]; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { + .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', + function($animate, $injector, $modalStack) { return { - restrict: 'EA', - replace: true, - templateUrl: 'template/modal/backdrop.html', - compile: function (tElement, tAttrs) { + restrict: 'A', + compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { - scope.animate = false; + if (attrs.modalInClass) { + $animate.addClass(element, attrs.modalInClass); - //trigger CSS transitions - $timeout(function () { - scope.animate = true; - }); + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + if (scope.modalOptions.animation) { + $animate.removeClass(element, attrs.modalInClass).then(done); + } else { + done(); + } + }); + } } }]) - .directive('modalWindow', ['$modalStack', '$q', function ($modalStack, $q) { + .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', + function($modalStack, $q, $animateCss, $document) { return { - restrict: 'EA', scope: { - index: '@', - animate: '=' + index: '@' }, - replace: true, + restrict: 'A', transclude: true, templateUrl: function(tElement, tAttrs) { - return tAttrs.templateUrl || 'template/modal/window.html'; + return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, - link: function (scope, element, attrs) { - element.addClass(attrs.windowClass || ''); + link: function(scope, element, attrs) { + element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; - scope.close = function (evt) { + scope.close = function(evt) { var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + if (modal && modal.value.backdrop && + modal.value.backdrop !== 'static' && + evt.target === evt.currentTarget) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; + // moved from template to fix issue #2280 + element.on('click', scope.close); + // This property is only added to the scope for the purpose of detecting when this directive is rendered. // We can detect that by using this property in the template associated with this directive and then use // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; - // Deferred object that will be resolved when this modal is render. + // Deferred object that will be resolved when this modal is rendered. var modalRenderDeferObj = $q.defer(); - // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. - // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. - attrs.$observe('modalRender', function (value) { - if (value == 'true') { - modalRenderDeferObj.resolve(); - } + // Resolve render promise post-digest + scope.$$postDigest(function() { + modalRenderDeferObj.resolve(); }); - modalRenderDeferObj.promise.then(function () { - // trigger CSS transitions - scope.animate = true; - - var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]'); - /** - * Auto-focusing of a freshly-opened modal element causes any child elements - * with the autofocus attribute to lose focus. This is an issue on touch - * based devices which will show and then hide the onscreen keyboard. - * Attempts to refocus the autofocus element via JavaScript will not reopen - * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus - * the modal element if the modal does not contain an autofocus element. - */ - if (inputsWithAutofocus.length) { - inputsWithAutofocus[0].focus(); - } else { - element[0].focus(); - } + modalRenderDeferObj.promise.then(function() { + var animationPromise = null; - // Notify {@link $modalStack} that modal is rendered. - var modal = $modalStack.getTop(); - if (modal) { - $modalStack.modalRendered(modal.key); + if (attrs.modalInClass) { + animationPromise = $animateCss(element, { + addClass: attrs.modalInClass + }).start(); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + $animateCss(element, { + removeClass: attrs.modalInClass + }).start().then(done); + }); } + + + $q.when(animationPromise).then(function() { + // Notify {@link $modalStack} that modal is rendered. + var modal = $modalStack.getTop(); + if (modal) { + $modalStack.modalRendered(modal.key); + } + + /** + * If something within the freshly-opened modal already has focus (perhaps via a + * directive that causes focus) then there's no need to try to focus anything. + */ + if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { + var inputWithAutofocus = element[0].querySelector('[autofocus]'); + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (inputWithAutofocus) { + inputWithAutofocus.focus(); + } else { + element[0].focus(); + } + } + }); }); } }; }]) - .directive('modalAnimationClass', [ - function () { - return { - compile: function (tElement, tAttrs) { - if (tAttrs.modalAnimation) { - tElement.addClass(tAttrs.modalAnimationClass); - } + .directive('uibModalAnimationClass', function() { + return { + compile: function(tElement, tAttrs) { + if (tAttrs.modalAnimation) { + tElement.addClass(tAttrs.uibModalAnimationClass); } - }; - }]) + } + }; + }) - .directive('modalTransclude', function () { + .directive('uibModalTransclude', ['$animate', function($animate) { return { - link: function($scope, $element, $attrs, controller, $transclude) { - $transclude($scope.$parent, function(clone) { - $element.empty(); - $element.append(clone); + link: function(scope, element, attrs, controller, transclude) { + transclude(scope.$parent, function(clone) { + element.empty(); + $animate.enter(clone, element); }); } }; - }) - - .factory('$modalStack', ['$animate', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', - function ($animate, $timeout, $document, $compile, $rootScope, $$stackedMap) { + }]) + .factory('$uibModalStack', ['$animate', '$animateCss', '$document', + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; + var openedClasses = $$multiMap.createNew(); + var $modalStack = { + NOW_CLOSING_EVENT: 'modal.stack.now-closing' + }; + var topModalIndex = 0; + var previousTopOpenedModal = null; + var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; + + //Modal focus behavior + var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + + 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' + + 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]'; + var scrollbarPadding; + var SNAKE_CASE_REGEXP = /[A-Z]/g; + + // TODO: extract into common dependency with tooltip + function snake_case(name) { + var separator = '-'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + function isVisible(element) { + return !!(element.offsetWidth || + element.offsetHeight || + element.getClientRects().length); + } function backdropIndex() { var topBackdropIndex = -1; @@ -2180,62 +4007,98 @@ angular.module('ui.bootstrap.modal', []) topBackdropIndex = i; } } + + // If any backdrop exist, ensure that it's index is always + // right below the top modal + if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { + topBackdropIndex = topModalIndex; + } return topBackdropIndex; } - $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + $rootScope.$watch(backdropIndex, function(newBackdropIndex) { if (backdropScope) { backdropScope.index = newBackdropIndex; } }); function removeModalWindow(modalInstance, elementToReceiveFocus) { - - var body = $document.find('body').eq(0); var modalWindow = openedWindows.get(modalInstance).value; + var appendToElement = modalWindow.appendTo; //clean up the stack openedWindows.remove(modalInstance); + previousTopOpenedModal = openedWindows.top(); + if (previousTopOpenedModal) { + topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); + } - //remove window DOM element removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - checkRemoveBackdrop(); - }); + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + var areAnyOpen = openedClasses.hasKey(modalBodyClass); + appendToElement.toggleClass(modalBodyClass, areAnyOpen); + if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); + } + scrollbarPadding = null; + } + toggleTopWindowClass(true); + }, modalWindow.closedDeferred); + checkRemoveBackdrop(); //move focus to specified element if available, or else to body if (elementToReceiveFocus && elementToReceiveFocus.focus) { elementToReceiveFocus.focus(); - } else { - body.focus(); + } else if (appendToElement.focus) { + appendToElement.focus(); } } - function checkRemoveBackdrop() { - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - var backdropScopeRef = backdropScope; - removeAfterAnimate(backdropDomEl, backdropScope, function () { - backdropScopeRef = null; - }); - backdropDomEl = undefined; - backdropScope = undefined; - } - } + // Add or remove "windowTopClass" from the top window in the stack + function toggleTopWindowClass(toggleSwitch) { + var modalWindow; - function removeAfterAnimate(domEl, scope, done) { - // Closing animation - scope.animate = false; + if (openedWindows.length() > 0) { + modalWindow = openedWindows.top().value; + modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); + } + } - if (domEl.attr('modal-animation') && $animate.enabled()) { - // transition out - domEl.one('$animate:close', function closeFn() { - $rootScope.$evalAsync(afterAnimating); + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() === -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, function() { + backdropScopeRef = null; }); - } else { - // Ensure this call is async - $timeout(afterAnimating); + backdropDomEl = undefined; + backdropScope = undefined; } + } + + function removeAfterAnimate(domEl, scope, done, closedDeferred) { + var asyncDeferred; + var asyncPromise = null; + var setIsAsync = function() { + if (!asyncDeferred) { + asyncDeferred = $q.defer(); + asyncPromise = asyncDeferred.promise; + } + + return function asyncDone() { + asyncDeferred.resolve(); + }; + }; + scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); + + // Note that it's intentional that asyncPromise might be null. + // That's when setIsAsync has not been called during the + // NOW_CLOSING_EVENT broadcast. + return $q.when(asyncPromise).then(afterAnimating); function afterAnimating() { if (afterAnimating.done) { @@ -2243,91 +4106,244 @@ angular.module('ui.bootstrap.modal', []) } afterAnimating.done = true; - domEl.remove(); + $animate.leave(domEl).then(function() { + if (done) { + done(); + } + + domEl.remove(); + if (closedDeferred) { + closedDeferred.resolve(); + } + }); + scope.$destroy(); - if (done) { - done(); - } } } - $document.bind('keydown', function (evt) { - var modal; + $document.on('keydown', keydownListener); - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - evt.preventDefault(); - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key, 'escape key press'); - }); + $rootScope.$on('$destroy', function() { + $document.off('keydown', keydownListener); + }); + + function keydownListener(evt) { + if (evt.isDefaultPrevented()) { + return evt; + } + + var modal = openedWindows.top(); + if (modal) { + switch (evt.which) { + case 27: { + if (modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function() { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + break; + } + case 9: { + var list = $modalStack.loadFocusElementList(modal); + var focusChanged = false; + if (evt.shiftKey) { + if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { + focusChanged = $modalStack.focusLastFocusableElement(list); + } + } else { + if ($modalStack.isFocusInLastItem(evt, list)) { + focusChanged = $modalStack.focusFirstFocusableElement(list); + } + } + + if (focusChanged) { + evt.preventDefault(); + evt.stopPropagation(); + } + + break; + } } } - }); + } - $modalStack.open = function (modalInstance, modal) { + $modalStack.open = function(modalInstance, modal) { + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; - var modalOpener = $document[0].activeElement; + toggleTopWindowClass(false); + + // Store the current top first, to determine what index we ought to use + // for the current top modal + previousTopOpenedModal = openedWindows.top(); openedWindows.add(modalInstance, { deferred: modal.deferred, renderDeferred: modal.renderDeferred, + closedDeferred: modal.closedDeferred, modalScope: modal.scope, backdrop: modal.backdrop, - keyboard: modal.keyboard + keyboard: modal.keyboard, + openedClass: modal.openedClass, + windowTopClass: modal.windowTopClass, + animation: modal.animation, + appendTo: modal.appendTo }); - var body = $document.find('body').eq(0), + openedClasses.put(modalBodyClass, modalInstance); + + var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); + backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; - var angularBackgroundDomEl = angular.element('
'); - angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = angular.element('
'); + backdropDomEl.attr({ + 'class': 'modal-backdrop', + 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}', + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }); + if (modal.backdropClass) { + backdropDomEl.addClass(modal.backdropClass); + } + if (modal.animation) { - angularBackgroundDomEl.attr('modal-animation', 'true'); + backdropDomEl.attr('modal-animation', 'true'); + } + $compile(backdropDomEl)(backdropScope); + $animate.enter(backdropDomEl, appendToElement); + if ($uibPosition.isScrollable(appendToElement)) { + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } } - backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); - body.append(backdropDomEl); } - var angularDomEl = angular.element('
'); + var content; + if (modal.component) { + content = document.createElement(snake_case(modal.component.name)); + content = angular.element(content); + content.attr({ + resolve: '$resolve', + 'modal-instance': '$uibModalInstance', + close: '$close($value)', + dismiss: '$dismiss($value)' + }); + } else { + content = modal.content; + } + + // Set the top modal index based on the index of the previous top modal + topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; + var angularDomEl = angular.element('
'); angularDomEl.attr({ + 'class': 'modal', 'template-url': modal.windowTemplateUrl, - 'window-class': modal.windowClass, + 'window-top-class': modal.windowTopClass, + 'role': 'dialog', + 'aria-labelledby': modal.ariaLabelledBy, + 'aria-describedby': modal.ariaDescribedBy, 'size': modal.size, - 'index': openedWindows.length() - 1, - 'animate': 'animate' - }).html(modal.content); + 'index': topModalIndex, + 'animate': 'animate', + 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}', + 'tabindex': -1, + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }).append(content); + if (modal.windowClass) { + angularDomEl.addClass(modal.windowClass); + } + if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; + appendToElement.addClass(modalBodyClass); + if (modal.scope) { + // we need to explicitly add the modal index to the modal scope + // because it is needed by ngStyle to compute the zIndex property. + modal.scope.$$topModalIndex = topModalIndex; + } + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); + + openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); + + applyAriaHidden(angularDomEl); + + function applyAriaHidden(el) { + if (!el || el[0].tagName === 'BODY') { + return; + } + + getSiblings(el).forEach(function(sibling) { + var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', + ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); + + if (!ariaHiddenCount) { + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + } + + sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); + sibling.setAttribute('aria-hidden', 'true'); + }); + + return applyAriaHidden(el.parent()); + + function getSiblings(el) { + var children = el.parent() ? el.parent().children() : []; + + return Array.prototype.filter.call(children, function(child) { + return child !== el[0]; + }); + } + } }; function broadcastClosing(modalWindow, resultOrReason, closing) { - return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + } + + function unhideBackgroundElements() { + Array.prototype.forEach.call( + document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), + function(hiddenEl) { + var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), + newHiddenCount = ariaHiddenCount - 1; + hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); + + if (!newHiddenCount) { + hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); + hiddenEl.removeAttribute('aria-hidden'); + } + } + ); } - $modalStack.close = function (modalInstance, result) { + $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, result, true)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } + return !modalWindow; }; - $modalStack.dismiss = function (modalInstance, reason) { + $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; @@ -2335,65 +4351,113 @@ angular.module('ui.bootstrap.modal', []) return !modalWindow; }; - $modalStack.dismissAll = function (reason) { + $modalStack.dismissAll = function(reason) { var topModal = this.getTop(); while (topModal && this.dismiss(topModal.key, reason)) { topModal = this.getTop(); } }; - $modalStack.getTop = function () { + $modalStack.getTop = function() { return openedWindows.top(); }; - $modalStack.modalRendered = function (modalInstance) { + $modalStack.modalRendered = function(modalInstance) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.renderDeferred.resolve(); } }; + $modalStack.focusFirstFocusableElement = function(list) { + if (list.length > 0) { + list[0].focus(); + return true; + } + return false; + }; + + $modalStack.focusLastFocusableElement = function(list) { + if (list.length > 0) { + list[list.length - 1].focus(); + return true; + } + return false; + }; + + $modalStack.isModalFocused = function(evt, modalWindow) { + if (evt && modalWindow) { + var modalDomEl = modalWindow.value.modalDomEl; + if (modalDomEl && modalDomEl.length) { + return (evt.target || evt.srcElement) === modalDomEl[0]; + } + } + return false; + }; + + $modalStack.isFocusInFirstItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[0]; + } + return false; + }; + + $modalStack.isFocusInLastItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[list.length - 1]; + } + return false; + }; + + $modalStack.loadFocusElementList = function(modalWindow) { + if (modalWindow) { + var modalDomE1 = modalWindow.value.modalDomEl; + if (modalDomE1 && modalDomE1.length) { + var elements = modalDomE1[0].querySelectorAll(tabbableSelector); + return elements ? + Array.prototype.filter.call(elements, function(element) { + return isVisible(element); + }) : elements; + } + } + }; + return $modalStack; }]) - .provider('$modal', function () { - + .provider('$uibModal', function() { var $modalProvider = { options: { animation: true, backdrop: true, //can also be false or 'static' keyboard: true }, - $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack', - function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) { - + $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', + function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : - $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl); - } - - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); - } - }); - return promisesArr; + $templateRequest(angular.isFunction(options.templateUrl) ? + options.templateUrl() : options.templateUrl); } - $modal.open = function (modalOptions) { + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); + var modalClosedDeferred = $q.defer(); var modalRenderDeferred = $q.defer(); //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = { result: modalResultDeferred.promise, opened: modalOpenedDeferred.promise, + closed: modalClosedDeferred.promise, rendered: modalRenderDeferred.promise, close: function (result) { return $modalStack.close(modalInstance, result); @@ -2406,287 +4470,428 @@ angular.module('ui.bootstrap.modal', []) //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions); modalOptions.resolve = modalOptions.resolve || {}; + modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } //verify options - if (!modalOptions.template && !modalOptions.templateUrl) { - throw new Error('One of template or templateUrl options is required.'); + if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of component or template or templateUrl options is required.'); } - var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + var templateAndResolvePromise; + if (modalOptions.component) { + templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null)); + } else { + templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + } + function resolveWithTemplate() { + return templateAndResolvePromise; + } - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + // Wait for the resolution of the existing promise chain. + // Then switch to our own combined promise dependency (regardless of how the previous modal fared). + // Then add to $modalStack and resolve opened. + // Finally clean up the chain variable if no subsequent modal has overwritten it. + var samePromise; + samePromise = promiseChain = $q.all([promiseChain]) + .then(resolveWithTemplate, resolveWithTemplate) + .then(function resolveSuccess(tplAndVars) { + var providedScope = modalOptions.scope || $rootScope; + + var modalScope = providedScope.$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + modalScope.$on('$destroy', function() { + if (!modalScope.$$uibDestructionScheduled) { + modalScope.$dismiss('$uibUnscheduledDestruction'); + } + }); - var modalScope = (modalOptions.scope || $rootScope).$new(); - modalScope.$close = modalInstance.close; - modalScope.$dismiss = modalInstance.dismiss; + var modal = { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + ariaLabelledBy: modalOptions.ariaLabelledBy, + ariaDescribedBy: modalOptions.ariaDescribedBy, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }; + + var component = {}; + var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; + + if (modalOptions.component) { + constructLocals(component, false, true, false); + component.name = modalOptions.component; + modal.component = component; + } else if (modalOptions.controller) { + constructLocals(ctrlLocals, true, false, true); + + // the third param will make the controller instantiate later,private api + // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 + ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs); + if (modalOptions.controllerAs && modalOptions.bindToController) { + ctrlInstance = ctrlInstantiate.instance; + ctrlInstance.$close = modalScope.$close; + ctrlInstance.$dismiss = modalScope.$dismiss; + angular.extend(ctrlInstance, { + $resolve: ctrlLocals.$scope.$resolve + }, providedScope); + } - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; + ctrlInstance = ctrlInstantiate(); - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; - }); + if (angular.isFunction(ctrlInstance.$onInit)) { + ctrlInstance.$onInit(); + } + } - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - if (modalOptions.controllerAs) { - modalScope[modalOptions.controllerAs] = ctrlInstance; + if (!modalOptions.component) { + modal.content = tplAndVars[0]; } - } - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - renderDeferred: modalRenderDeferred, - content: tplAndVars[0], - animation: modalOptions.animation, - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - backdropClass: modalOptions.backdropClass, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size - }); + $modalStack.open(modalInstance, modal); + modalOpenedDeferred.resolve(true); - }, function resolveError(reason) { - modalResultDeferred.reject(reason); - }); + function constructLocals(obj, template, instanceOnScope, injectable) { + obj.$scope = modalScope; + obj.$scope.$resolve = {}; + if (instanceOnScope) { + obj.$scope.$uibModalInstance = modalInstance; + } else { + obj.$uibModalInstance = modalInstance; + } - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function (reason) { + var resolves = template ? tplAndVars[1] : tplAndVars; + angular.forEach(resolves, function(value, key) { + if (injectable) { + obj[key] = value; + } + + obj.$scope.$resolve[key] = value; + }); + } + }, function resolveError(reason) { modalOpenedDeferred.reject(reason); + modalResultDeferred.reject(reason); + })['finally'](function() { + if (promiseChain === samePromise) { + promiseChain = null; + } }); return modalInstance; }; return $modal; - }] + } + ] }; return $modalProvider; }); -angular.module('ui.bootstrap.pagination', []) +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ +.factory('uibPaging', ['$parse', function($parse) { + return { + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + ctrl._watchers = []; + + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; + + ngModelCtrl.$render = function() { + ctrl.render(); + }; + + if ($attrs.itemsPerPage) { + ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + })); + } else { + ctrl.itemsPerPage = config.itemsPerPage; + } + + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; + + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; -.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); + } - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); + } + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); + } + }; - ngModelCtrl.$render = function() { - self.render(); - }; + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } + $scope.noPrevious = function() { + return $scope.page === 1; + }; - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable - if ( $scope.page > value ) { - $scope.selectPage(value); - } else { - ngModelCtrl.$render(); - } - }); - }; + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); + $scope.$on('$destroy', function() { + while (ctrl._watchers.length) { + ctrl._watchers.shift()(); + } + }); + } }; +}]); - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) - $scope.selectPage = function(page, evt) { - if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { - if (evt && evt.target) { - evt.target.blur(); - } - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); - } - }; +.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; - $scope.getText = function( key ) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; + uibPaging.create(this, $scope, $attrs); }]) -.constant('paginationConfig', { +.constant('uibPagerConfig', { itemsPerPage: 10, - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true + previousText: '« Previous', + nextText: 'Next »', + align: true }) -.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { +.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { return { - restrict: 'EA', scope: { totalItems: '=', - firstText: '@', previousText: '@', nextText: '@', - lastText: '@' + ngDisabled: '=' + }, + require: ['uibPager', '?ngModel'], + restrict: 'A', + controller: 'UibPagerController', + controllerAs: 'pager', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pager/pager.html'; }, - require: ['pagination', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pager'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { - return; // do nothing if no ng-model + return; // do nothing if no ng-model } - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } + paginationCtrl.init(ngModelCtrl, uibPagerConfig); + } + }; +}]); - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive - }; - } +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) +.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers, + pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + $attrs.$set('role', 'menu'); + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + })); + } - function getPages(currentPage, totalPages) { - var pages = []; + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + function getPages(currentPage, totalPages) { + var pages = []; - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); - endPage = startPage + maxSize - 1; + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); - } + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); - } + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, pageLabel(number), number === currentPage); + pages.push(page); + } - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); } - - return pages; } - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); } - }; + } + } + return pages; + } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); } }; }]) -.constant('pagerConfig', { +.constant('uibPaginationConfig', { itemsPerPage: 10, - previousText: '« Previous', - nextText: 'Next »', - align: true + boundaryLinks: false, + boundaryLinkNumbers: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true, + forceEllipses: false }) -.directive('pager', ['pagerConfig', function(pagerConfig) { +.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { return { - restrict: 'EA', scope: { totalItems: '=', + firstText: '@', previousText: '@', - nextText: '@' + nextText: '@', + lastText: '@', + ngDisabled:'=' + }, + require: ['uibPagination', '?ngModel'], + restrict: 'A', + controller: 'UibPaginationController', + controllerAs: 'pagination', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pagination/pagination.html'; }, - require: ['pager', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pagination'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { return; // do nothing if no ng-model } - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); } }; }]); @@ -2696,18 +4901,20 @@ angular.module('ui.bootstrap.pagination', []) * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ -.provider( '$tooltip', function () { +.provider('$uibTooltip', function() { // The default options tooltip and popover. var defaultOptions = { placement: 'top', + placementClassPrefix: '', animation: true, popupDelay: 0, + popupCloseDelay: 0, useContentExp: false }; @@ -2715,7 +4922,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap var triggerMap = { 'mouseenter': 'mouseleave', 'click': 'click', - 'focus': 'blur' + 'outsideClick': 'outsideClick', + 'focus': 'blur', + 'none': '' }; // The options specified to the provider globally. @@ -2730,23 +4939,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * $tooltipProvider.options( { placement: 'left' } ); * }); */ - this.options = function( value ) { - angular.extend( globalOptions, value ); + this.options = function(value) { + angular.extend(globalOptions, value); }; /** * This allows you to extend the set of trigger mappings available. E.g.: * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); }; /** - * This is a helper function for translating camel-case to snake-case. + * This is a helper function for translating camel-case to snake_case. */ - function snake_case(name){ + function snake_case(name) { var regexp = /[A-Z]/g; var separator = '-'; return name.replace(regexp, function(letter, pos) { @@ -2758,9 +4967,26 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ - this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow, options ) { - options = angular.extend( {}, defaultOptions, globalOptions, options ); + this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keyup', keypressListener); + + $rootScope.$on('$destroy', function() { + $document.off('keyup', keypressListener); + }); + + function keypressListener(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + last = null; + } + } + } + + return function $tooltip(ttType, prefix, defaultTriggerShow, options) { + options = angular.extend({}, defaultOptions, globalOptions, options); /** * Returns an object of show and hide triggers. @@ -2776,57 +5002,102 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; + function getTriggers(trigger) { + var show = (trigger || options.trigger || defaultTriggerShow).split(' '); + var hide = show.map(function(trigger) { + return triggerMap[trigger] || trigger; + }); return { show: show, hide: hide }; } - var directiveName = snake_case( type ); + var directiveName = snake_case(ttType); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); var template = - '
'+ + 'content="' + startSym + 'content' + endSym + '" ') + + 'origin-scope="origScope" ' + + 'class="uib-position-measure ' + prefix + '" ' + + 'tooltip-animation-class="fade"' + + 'uib-tooltip-classes ' + + 'ng-class="{ in: isOpen }" ' + + '>' + '
'; return { - restrict: 'EA', - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); + compile: function(tElem, tAttrs) { + var tooltipLinker = $compile(template); - return function link ( scope, element, attrs, tooltipCtrl ) { + return function link(scope, element, attrs, tooltipCtrl) { var tooltip; var tooltipLinkedScope; var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var showTimeout; + var hideTimeout; + var positionTimeout; + var adjustmentTimeout; + var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; + var triggers = getTriggers(undefined); + var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); var ttScope = scope.$new(true); + var repositionScheduled = false; + var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; + var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; + var observers = []; + var lastPlacement; + + var positionTooltip = function() { + // check if tooltip exists and is not empty + if (!tooltip || !tooltip.html()) { return; } + + if (!positionTimeout) { + positionTimeout = $timeout(function() { + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var elementPos = appendToBody ? $position.offset(element) : $position.position(element); + tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); + var placementClasses = ttPosition.placement.split('-'); + + if (!tooltip.hasClass(placementClasses[0])) { + tooltip.removeClass(lastPlacement.split('-')[0]); + tooltip.addClass(placementClasses[0]); + } - var positionTooltip = function () { - if (!tooltip) { return; } + if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { + tooltip.removeClass(options.placementClassPrefix + lastPlacement); + tooltip.addClass(options.placementClassPrefix + ttPosition.placement); + } - var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; + adjustmentTimeout = $timeout(function() { + var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight); + if (adjustment) { + tooltip.css(adjustment); + } + adjustmentTimeout = null; + }, 0, false); + + // first time through tt element will have the + // uib-position-measure class or if the placement + // has changed we need to position the arrow. + if (tooltip.hasClass('uib-position-measure')) { + $position.positionArrow(tooltip, ttPosition.placement); + tooltip.removeClass('uib-position-measure'); + } else if (lastPlacement !== ttPosition.placement) { + $position.positionArrow(tooltip, ttPosition.placement); + } + lastPlacement = ttPosition.placement; - // Now set the calculated positioning. - tooltip.css( ttPosition ); + positionTimeout = null; + }, 0, false); + } }; // Set up the correct scope to allow transclusion later @@ -2836,8 +5107,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // TODO add ability to start tooltip opened ttScope.isOpen = false; - function toggleTooltipBind () { - if ( ! ttScope.isOpen ) { + function toggleTooltipBind() { + if (!ttScope.isOpen) { showTooltipBind(); } else { hideTooltipBind(); @@ -2846,213 +5117,360 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { return; } + cancelHide(); prepareTooltip(); - if ( ttScope.popupDelay ) { + if (ttScope.popupDelay) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, ttScope.popupDelay, false ); - popupTimeout.then(function(reposition){reposition();}); + if (!showTimeout) { + showTimeout = $timeout(show, ttScope.popupDelay, false); } } else { - show()(); + show(); } } - function hideTooltipBind () { - scope.$apply(function () { + function hideTooltipBind() { + cancelShow(); + + if (ttScope.popupCloseDelay) { + if (!hideTimeout) { + hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); + } + } else { hide(); - }); + } } // Show the tooltip popup element. function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } + cancelShow(); + cancelHide(); // Don't show empty tooltips. - if ( !(options.useContentExp ? ttScope.contentExp() : ttScope.content) ) { + if (!ttScope.content) { return angular.noop; } createTooltip(); - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - ttScope.$digest(); - - positionTooltip(); - // And show the tooltip. - ttScope.isOpen = true; - ttScope.$apply(); // digest required as $apply is not called + ttScope.$evalAsync(function() { + ttScope.isOpen = true; + assignIsOpen(true); + positionTooltip(); + }); + } - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; + function cancelShow() { + if (showTimeout) { + $timeout.cancel(showTimeout); + showTimeout = null; + } + + if (positionTimeout) { + $timeout.cancel(positionTimeout); + positionTimeout = null; + } } // Hide the tooltip popup element. function hide() { + if (!ttScope) { + return; + } + // First things first: we don't show it anymore. - ttScope.isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( ttScope.animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); + ttScope.$evalAsync(function() { + if (ttScope) { + ttScope.isOpen = false; + assignIsOpen(false); + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + // The fade transition in TWBS is 150ms. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 150, false); + } + } else { + removeTooltip(); + } } - } else { - removeTooltip(); + }); + } + + function cancelHide() { + if (hideTimeout) { + $timeout.cancel(hideTimeout); + hideTimeout = null; + } + + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; } } function createTooltip() { // There can only be one tooltip element per directive shown at once. if (tooltip) { - removeTooltip(); + return; } + tooltipLinkedScope = ttScope.$new(); - tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); } else { - element.after( tooltip ); + element.after(tooltip); } }); - tooltipLinkedScope.$watch(function () { - $timeout(positionTooltip, 0, false); + openedTooltips.add(ttScope, { + close: hide }); - if (options.useContentExp) { - tooltipLinkedScope.$watch('contentExp()', function (val) { - if (!val && ttScope.isOpen ) { - hide(); - } - }); - } + prepObservers(); } function removeTooltip() { - transitionTimeout = null; + cancelShow(); + cancelHide(); + unregisterObservers(); + if (tooltip) { tooltip.remove(); + tooltip = null; + if (adjustmentTimeout) { + $timeout.cancel(adjustmentTimeout); + } } + + openedTooltips.remove(ttScope); + if (tooltipLinkedScope) { tooltipLinkedScope.$destroy(); tooltipLinkedScope = null; } } + /** + * Set the initial scope values. Once + * the tooltip is created, the observers + * will be added to keep things in sync. + */ function prepareTooltip() { - prepPopupClass(); - prepPlacement(); - prepPopupDelay(); + ttScope.title = attrs[prefix + 'Title']; + if (contentParse) { + ttScope.content = contentParse(scope); + } else { + ttScope.content = attrs[ttType]; + } + + ttScope.popupClass = attrs[prefix + 'Class']; + ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; + var placement = $position.parsePlacement(ttScope.placement); + lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; + + var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); + var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; } - ttScope.contentExp = function () { - return scope.$eval(attrs[type]); + function assignIsOpen(isOpen) { + if (isOpenParse && angular.isFunction(isOpenParse.assign)) { + isOpenParse.assign(scope, isOpen); + } + } + + ttScope.contentExp = function() { + return ttScope.content; }; /** * Observe the relevant attributes. */ - if (!options.useContentExp) { - attrs.$observe( type, function ( val ) { - ttScope.content = val; + attrs.$observe('disabled', function(val) { + if (val) { + cancelShow(); + } + + if (val && ttScope.isOpen) { + hide(); + } + }); - if (!val && ttScope.isOpen ) { - hide(); + if (isOpenParse) { + scope.$watch(isOpenParse, function(val) { + if (ttScope && !val === ttScope.isOpen) { + toggleTooltipBind(); } }); } - attrs.$observe( 'disabled', function ( val ) { - if (val && ttScope.isOpen ) { - hide(); + function prepObservers() { + observers.length = 0; + + if (contentParse) { + observers.push( + scope.$watch(contentParse, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } + }) + ); + + observers.push( + tooltipLinkedScope.$watch(function() { + if (!repositionScheduled) { + repositionScheduled = true; + tooltipLinkedScope.$$postDigest(function() { + repositionScheduled = false; + if (ttScope && ttScope.isOpen) { + positionTooltip(); + } + }); + } + }) + ); + } else { + observers.push( + attrs.$observe(ttType, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } else { + positionTooltip(); + } + }) + ); } - }); - attrs.$observe( prefix+'Title', function ( val ) { - ttScope.title = val; - }); + observers.push( + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + + observers.push( + attrs.$observe(prefix + 'Placement', function(val) { + ttScope.placement = val ? val : options.placement; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + } - function prepPopupClass() { - ttScope.popupClass = attrs[prefix + 'Class']; + function unregisterObservers() { + if (observers.length) { + angular.forEach(observers, function(observer) { + observer(); + }); + observers.length = 0; + } } - function prepPlacement() { - var val = attrs[ prefix + 'Placement' ]; - ttScope.placement = angular.isDefined( val ) ? val : options.placement; + // hide tooltips/popovers for outsideClick trigger + function bodyHideTooltipBind(e) { + if (!ttScope || !ttScope.isOpen || !tooltip) { + return; + } + // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked + if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { + hideTooltipBind(); + } } - function prepPopupDelay() { - var val = attrs[ prefix + 'PopupDelay' ]; - var delay = parseInt( val, 10 ); - ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + // KeyboardEvent handler to hide the tooltip on Escape key press + function hideOnEscapeKey(e) { + if (e.which === 27) { + hideTooltipBind(); + } } - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); + var unregisterTriggers = function() { + triggers.show.forEach(function(trigger) { + if (trigger === 'outsideClick') { + element.off('click', toggleTooltipBind); + } else { + element.off(trigger, showTooltipBind); + element.off(trigger, toggleTooltipBind); + } + element.off('keypress', hideOnEscapeKey); + }); + triggers.hide.forEach(function(trigger) { + if (trigger === 'outsideClick') { + $document.off('click', bodyHideTooltipBind); + } else { + element.off(trigger, hideTooltipBind); + } + }); }; function prepTriggers() { - var val = attrs[ prefix + 'Trigger' ]; + var showTriggers = [], hideTriggers = []; + var val = scope.$eval(attrs[prefix + 'Trigger']); unregisterTriggers(); - triggers = getTriggers( val ); - - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); + if (angular.isObject(val)) { + Object.keys(val).forEach(function(key) { + showTriggers.push(key); + hideTriggers.push(val[key]); + }); + triggers = { + show: showTriggers, + hide: hideTriggers + }; } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); + triggers = getTriggers(val); + } + + if (triggers.show !== 'none') { + triggers.show.forEach(function(trigger, idx) { + if (trigger === 'outsideClick') { + element.on('click', toggleTooltipBind); + $document.on('click', bodyHideTooltipBind); + } else if (trigger === triggers.hide[idx]) { + element.on(trigger, toggleTooltipBind); + } else if (trigger) { + element.on(trigger, showTooltipBind); + element.on(triggers.hide[idx], hideTooltipBind); + } + element.on('keypress', hideOnEscapeKey); + }); } } + prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); - appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( ttScope.isOpen ) { - hide(); - } - }); + var appendToBodyVal; + var appendKey = prefix + 'AppendToBody'; + if (appendKey in attrs && attrs[appendKey] === undefined) { + appendToBodyVal = true; + } else { + appendToBodyVal = scope.$eval(attrs[appendKey]); } + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); ttScope = null; @@ -3065,11 +5483,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }) // This is mostly ngInclude code but with a custom scope -.directive( 'tooltipTemplateTransclude', [ +.directive('uibTooltipTemplateTransclude', [ '$animate', '$sce', '$compile', '$templateRequest', -function ($animate , $sce , $compile , $templateRequest) { +function ($animate, $sce, $compile, $templateRequest) { return { - link: function ( scope, elem, attrs ) { + link: function(scope, elem, attrs) { var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); var changeCounter = 0, @@ -3082,10 +5500,12 @@ function ($animate , $sce , $compile , $templateRequest) { previousElement.remove(); previousElement = null; } + if (currentScope) { currentScope.$destroy(); currentScope = null; } + if (currentElement) { $animate.leave(currentElement).then(function() { previousElement = null; @@ -3095,7 +5515,7 @@ function ($animate , $sce , $compile , $templateRequest) { } }; - scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) { + scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { var thisChangeId = ++changeCounter; if (src) { @@ -3137,219 +5557,242 @@ function ($animate , $sce , $compile , $templateRequest) { * They must not be animated as they're expected to be present on the tooltip on * initialization. */ -.directive('tooltipClasses', function () { +.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { return { restrict: 'A', - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + // need to set the primary position so the + // arrow has space during position measure. + // tooltip.positionTooltip() if (scope.placement) { - element.addClass(scope.placement); + // // There are no top-left etc... classes + // // in TWBS, so we need the primary position. + var position = $uibPosition.parsePlacement(scope.placement); + element.addClass(position[0]); } + if (scope.popupClass) { element.addClass(scope.popupClass); } - if (scope.animation()) { + + if (scope.animation) { element.addClass(attrs.tooltipAnimationClass); } } }; -}) +}]) -.directive( 'tooltipPopup', function () { +.directive('uibTooltipPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' + restrict: 'A', + scope: { content: '@' }, + templateUrl: 'uib/template/tooltip/tooltip-popup.html' }; }) -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); }]) -.directive( 'tooltipTemplatePopup', function () { +.directive('uibTooltipTemplatePopup', function() { return { - restrict: 'EA', - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, - templateUrl: 'template/tooltip/tooltip-template-popup.html' + restrict: 'A', + scope: { contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' }; }) -.directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) { - return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', { +.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { useContentExp: true }); }]) -.directive( 'tooltipHtmlPopup', function () { +.directive('uibTooltipHtmlPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-popup.html' + restrict: 'A', + scope: { contentExp: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' }; }) -.directive( 'tooltipHtml', [ '$tooltip', function ( $tooltip ) { - return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', { +.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { useContentExp: true }); -}]) - -/* -Deprecated -*/ -.directive( 'tooltipHtmlUnsafePopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' - }; -}) - -.value('tooltipHtmlUnsafeSuppressDeprecated', false) -.directive( 'tooltipHtmlUnsafe', [ - '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log', -function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) { - if (!tooltipHtmlUnsafeSuppressDeprecated) { - $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.'); - } - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. + * just mouse enter/leave, and selector delegatation. */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) +angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) -.directive( 'popoverTemplatePopup', function () { +.directive('uibPopoverTemplatePopup', function() { return { - restrict: 'EA', - replace: true, - scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, - templateUrl: 'template/popover/popover-template.html' + restrict: 'A', + scope: { uibTitle: '@', contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/popover/popover-template.html' }; }) -.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popoverTemplate', 'popover', 'click', { +.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { useContentExp: true - } ); + }); }]) -.directive( 'popoverPopup', function () { +.directive('uibPopoverHtmlPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' + restrict: 'A', + scope: { contentExp: '&', uibTitle: '@' }, + templateUrl: 'uib/template/popover/popover-html.html' }; }) -.directive( 'popover', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); +.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverHtml', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverPopup', function() { + return { + restrict: 'A', + scope: { uibTitle: '@', content: '@' }, + templateUrl: 'uib/template/popover/popover.html' + }; +}) + +.directive('uibPopover', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopover', 'popover', 'click'); }]); angular.module('ui.bootstrap.progressbar', []) -.constant('progressConfig', { +.constant('uibProgressConfig', { animate: true, max: 100 }) -.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - var self = this, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; +.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - this.bars = []; - $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max; + this.bars = []; + $scope.max = getMaxOrDefault(); - this.addBar = function(bar, element) { - if ( !animate ) { - element.css({'transition': 'none'}); - } + this.addBar = function(bar, element, attrs) { + if (!animate) { + element.css({'transition': 'none'}); + } - this.bars.push(bar); + this.bars.push(bar); - bar.$watch('value', function( value ) { - bar.percent = +(100 * value / $scope.max).toFixed(2); - }); + bar.max = getMaxOrDefault(); + bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; - bar.$on('$destroy', function() { - element = null; - self.removeBar(bar); - }); - }; + bar.$watch('value', function(value) { + bar.recalculatePercentage(); + }); - this.removeBar = function(bar) { - this.bars.splice(this.bars.indexOf(bar), 1); + bar.recalculatePercentage = function() { + var totalPercentage = self.bars.reduce(function(total, bar) { + bar.percent = +(100 * bar.value / bar.max).toFixed(2); + return total + bar.percent; + }, 0); + + if (totalPercentage > 100) { + bar.percent -= totalPercentage - 100; + } }; + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + this.bars.forEach(function (bar) { + bar.recalculatePercentage(); + }); + }; + + //$attrs.$observe('maxParam', function(maxParam) { + $scope.$watch('maxParam', function(maxParam) { + self.bars.forEach(function(bar) { + bar.max = getMaxOrDefault(); + bar.recalculatePercentage(); + }); + }); + + function getMaxOrDefault () { + return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max; + } }]) -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - templateUrl: 'template/progressbar/progress.html' - }; +.directive('uibProgress', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + require: 'uibProgress', + scope: { + maxParam: '=?max' + }, + templateUrl: 'uib/template/progressbar/progress.html' + }; }) -.directive('bar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - require: '^progress', - scope: { - value: '=', - max: '=?', - type: '@' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, element); - } - }; +.directive('uibBar', function() { + return { + replace: true, + transclude: true, + require: '^uibProgress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'uib/template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element, attrs); + } + }; }) -.directive('progressbar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - scope: { - value: '=', - max: '=?', - type: '@' - }, - templateUrl: 'template/progressbar/progressbar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, angular.element(element.children()[0])); - } - }; +.directive('uibProgressbar', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + scope: { + value: '=', + maxParam: '=?max', + type: '@' + }, + templateUrl: 'uib/template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); + } + }; }); angular.module('ui.bootstrap.rating', []) -.constant('ratingConfig', { +.constant('uibRatingConfig', { max: 5, stateOn: null, - stateOff: null + stateOff: null, + enableReset: true, + titles: ['one', 'two', 'three', 'four', 'five'] }) -.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { - var ngModelCtrl = { $setViewValue: angular.noop }; +.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }, + self = this; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; @@ -3359,33 +5802,49 @@ angular.module('ui.bootstrap.rating', []) if (angular.isNumber(value) && value << 0 !== value) { value = Math.round(value); } + return value; }); this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; - - var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : - new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + this.enableReset = angular.isDefined($attrs.enableReset) ? + $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; + + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); $scope.range = this.buildTemplateObjects(ratingStates); }; this.buildTemplateObjects = function(states) { for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); } return states; }; + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } + + return this.titles[index]; + }; + $scope.rate = function(value) { - if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { - ngModelCtrl.$setViewValue(value); + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value; + ngModelCtrl.$setViewValue(newViewValue); ngModelCtrl.$render(); } }; $scope.enter = function(value) { - if ( !$scope.readonly ) { + if (!$scope.readonly) { $scope.value = value; } $scope.onHover({value: value}); @@ -3400,227 +5859,167 @@ angular.module('ui.bootstrap.rating', []) if (/(37|38|39|40)/.test(evt.which)) { evt.preventDefault(); evt.stopPropagation(); - $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); } }; this.render = function() { $scope.value = ngModelCtrl.$viewValue; + $scope.title = self.getTitle($scope.value - 1); }; }]) -.directive('rating', function() { +.directive('uibRating', function() { return { - restrict: 'EA', - require: ['rating', 'ngModel'], + require: ['uibRating', 'ngModel'], + restrict: 'A', scope: { - readonly: '=?', + readonly: '=?readOnly', onHover: '&', onLeave: '&' }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', - replace: true, + controller: 'UibRatingController', + templateUrl: 'uib/template/rating/rating.html', link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - ratingCtrl.init( ngModelCtrl ); + ratingCtrl.init(ngModelCtrl); } }; }); -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - angular.module('ui.bootstrap.tabs', []) -.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { +.controller('UibTabsetController', ['$scope', function ($scope) { var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; + oldIndex; + ctrl.tabs = []; + + ctrl.select = function(index, evt) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect({ + $event: evt, + $selectedIndex: index + }); + if (evt && evt.isDefaultPrevented()) { + return; + } + previousSelected.tab.active = false; + } - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect({ + $event: evt + }); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isDefined(oldIndex)) { + ctrl.active = null; + oldIndex = null; } - }); - selectedTab.active = true; - selectedTab.onSelect(); + } }; ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1 && tab.active !== false) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); - } - else { - tab.active = false; + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); } }; ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected and not destroyed - if (tab.active && tabs.length > 1 && !destroyed) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); + var index; + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].tab === tab) { + index = i; + break; + } } - tabs.splice(index, 1); + + if (ctrl.tabs[index].index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); + } + + ctrl.tabs.splice(index, 1); }; + $scope.$watch('tabset.active', function(val) { + if (angular.isDefined(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + var destroyed; $scope.$on('$destroy', function() { destroyed = true; }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * @param {boolean=} justified Whether or not to use justified styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
- - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
-
- */ -.directive('tabset', function() { +.directive('uibTabset', function() { return { - restrict: 'EA', transclude: true, replace: true, - scope: { + scope: {}, + bindToController: { + active: '=?', type: '@' }, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', + controller: 'UibTabsetController', + controllerAs: 'tabset', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tabset.html'; + }, link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; } }; }) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
- - -
- - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
-
- - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
- */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', '$log', function($parse, $log) { +.directive('uibTab', ['$parse', function($parse) { return { - require: '^tabset', - restrict: 'EA', + require: '^uibTabset', replace: true, - templateUrl: 'template/tabs/tab.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tab.html'; + }, transclude: true, scope: { - active: '=?', heading: '@', + index: '=?', + classes: '@?', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' @@ -3628,56 +6027,58 @@ angular.module('ui.bootstrap.tabs', []) controller: function() { //Empty controller so other directives can require being 'under' a tab }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } + controllerAs: 'tab', + link: function(scope, elm, attrs, tabsetCtrl, transclude) { + scope.disabled = false; + if (attrs.disable) { + scope.$parent.$watch($parse(attrs.disable), function(value) { + scope.disabled = !! value; }); + } - scope.disabled = false; - if ( attrs.disable ) { - scope.$parent.$watch($parse(attrs.disable), function(value) { - scope.disabled = !! value; - }); + if (angular.isUndefined(attrs.index)) { + if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) { + scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1; + } else { + scope.index = 0; } + } - // Deprecation support of "disabled" parameter - // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677 - // This code is duplicated from the lines above to make it easy to remove once - // the feature has been completely deprecated - if ( attrs.disabled ) { - $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"'); - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); - } + if (angular.isUndefined(attrs.classes)) { + scope.classes = ''; + } - scope.select = function() { - if ( !scope.disabled ) { - scope.active = true; + scope.select = function(evt) { + if (!scope.disabled) { + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } } - }; - - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. - scope.$transcludeFn = transclude; + tabsetCtrl.select(index, evt); + } }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; } }; }]) -.directive('tabHeadingTransclude', [function() { +.directive('uibTabHeadingTransclude', function() { return { restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { + require: '^uibTab', + link: function(scope, elm) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { elm.html(''); @@ -3686,14 +6087,14 @@ angular.module('ui.bootstrap.tabs', []) }); } }; -}]) +}) -.directive('tabContentTransclude', function() { +.directive('uibTabContentTransclude', function() { return { restrict: 'A', - require: '^tabset', + require: '^uibTabset', link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.tabContentTransclude); + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. @@ -3709,107 +6110,202 @@ angular.module('ui.bootstrap.tabs', []) }); } }; + function isTabHeading(node) { - return node.tagName && ( - node.hasAttribute('tab-heading') || - node.hasAttribute('data-tab-heading') || - node.tagName.toLowerCase() === 'tab-heading' || - node.tagName.toLowerCase() === 'data-tab-heading' + return node.tagName && ( + node.hasAttribute('uib-tab-heading') || + node.hasAttribute('data-uib-tab-heading') || + node.hasAttribute('x-uib-tab-heading') || + node.tagName.toLowerCase() === 'uib-tab-heading' || + node.tagName.toLowerCase() === 'data-uib-tab-heading' || + node.tagName.toLowerCase() === 'x-uib-tab-heading' || + node.tagName.toLowerCase() === 'uib:tab-heading' ); } -}) - -; +}); angular.module('ui.bootstrap.timepicker', []) -.constant('timepickerConfig', { +.constant('uibTimepickerConfig', { hourStep: 1, minuteStep: 1, + secondStep: 1, showMeridian: true, + showSeconds: false, meridians: null, readonlyInput: false, mousewheel: true, arrowkeys: true, - showSpinners: true + showSpinners: true, + templateUrl: 'uib/template/timepicker/timepicker.html' }) -.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { +.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { + var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl; var selected = new Date(), - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + watchers = [], + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS, + padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true; - this.init = function( ngModelCtrl_, inputs ) { + $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; + $element.removeAttr('tabindex'); + + this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; - ngModelCtrl.$formatters.unshift(function (modelValue) { - return modelValue ? new Date( modelValue ) : null; + ngModelCtrl.$formatters.unshift(function(modelValue) { + return modelValue ? new Date(modelValue) : null; }); var hoursInputEl = inputs.eq(0), - minutesInputEl = inputs.eq(1); + minutesInputEl = inputs.eq(1), + secondsInputEl = inputs.eq(2); + + hoursModelCtrl = hoursInputEl.controller('ngModel'); + minutesModelCtrl = minutesInputEl.controller('ngModel'); + secondsModelCtrl = secondsInputEl.controller('ngModel'); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + + if (mousewheel) { + this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); } var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; if (arrowkeys) { - this.setupArrowkeyEvents( hoursInputEl, minutesInputEl ); + this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; - this.setupInputEvents( hoursInputEl, minutesInputEl ); + this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); }; var hourStep = timepickerConfig.hourStep; if ($attrs.hourStep) { - $scope.$parent.$watch($parse($attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = +value; + })); } var minuteStep = timepickerConfig.minuteStep; if ($attrs.minuteStep) { - $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = +value; + })); + } + + var min; + watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) { + var dt = new Date(value); + min = isNaN(dt) ? undefined : dt; + })); + + var max; + watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) { + var dt = new Date(value); + max = isNaN(dt) ? undefined : dt; + })); + + var disabled = false; + if ($attrs.ngDisabled) { + watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { + disabled = value; + })); + } + + $scope.noIncrementHours = function() { + var incrementedSelected = addMinutes(selected, hourStep * 60); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementHours = function() { + var decrementedSelected = addMinutes(selected, -hourStep * 60); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementMinutes = function() { + var incrementedSelected = addMinutes(selected, minuteStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementMinutes = function() { + var decrementedSelected = addMinutes(selected, -minuteStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementSeconds = function() { + var incrementedSelected = addSeconds(selected, secondStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementSeconds = function() { + var decrementedSelected = addSeconds(selected, -secondStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noToggleMeridian = function() { + if (selected.getHours() < 12) { + return disabled || addMinutes(selected, 12 * 60) > max; + } + + return disabled || addMinutes(selected, -12 * 60) < min; + }; + + var secondStep = timepickerConfig.secondStep; + if ($attrs.secondStep) { + watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) { + secondStep = +value; + })); + } + + $scope.showSeconds = timepickerConfig.showSeconds; + if ($attrs.showSeconds) { + watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) { + $scope.showSeconds = !!value; + })); } // 12H / 24H mode $scope.showMeridian = timepickerConfig.showMeridian; if ($attrs.showMeridian) { - $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) { $scope.showMeridian = !!value; - if ( ngModelCtrl.$error.time ) { + if (ngModelCtrl.$error.time) { // Evaluate from template var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); refresh(); } } else { updateTemplate(); } - }); + })); } // Get $scope.hours in 24H mode if valid - function getHoursFromTemplate ( ) { - var hours = parseInt( $scope.hours, 10 ); - var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { + function getHoursFromTemplate() { + var hours = +$scope.hours; + var valid = $scope.showMeridian ? hours > 0 && hours < 13 : + hours >= 0 && hours < 24; + if (!valid || $scope.hours === '') { return undefined; } - if ( $scope.showMeridian ) { - if ( hours === 12 ) { + if ($scope.showMeridian) { + if (hours === 12) { hours = 0; } - if ( $scope.meridian === meridians[1] ) { + if ($scope.meridian === meridians[1]) { hours = hours + 12; } } @@ -3817,118 +6313,222 @@ angular.module('ui.bootstrap.timepicker', []) } function getMinutesFromTemplate() { - var minutes = parseInt($scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + var minutes = +$scope.minutes; + var valid = minutes >= 0 && minutes < 60; + if (!valid || $scope.minutes === '') { + return undefined; + } + return minutes; } - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value.toString(); + function getSecondsFromTemplate() { + var seconds = +$scope.seconds; + return seconds >= 0 && seconds < 60 ? seconds : undefined; + } + + function pad(value, noPad) { + if (value === null) { + return ''; + } + + return angular.isDefined(value) && value.toString().length < 2 && !noPad ? + '0' + value : value.toString(); } // Respond on mousewheel spin - this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; } //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); + var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; + return e.detail || delta > 0; }; - hoursInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + hoursInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); + } e.preventDefault(); }); - minutesInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + minutesInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); + } e.preventDefault(); }); + secondsInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); + } + e.preventDefault(); + }); }; // Respond on up/down arrowkeys - this.setupArrowkeyEvents = function( hoursInputEl, minutesInputEl ) { - hoursInputEl.bind('keydown', function(e) { - if ( e.which === 38 ) { // up - e.preventDefault(); - $scope.incrementHours(); - $scope.$apply(); - } - else if ( e.which === 40 ) { // down - e.preventDefault(); - $scope.decrementHours(); - $scope.$apply(); + this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + hoursInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementHours(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementHours(); + $scope.$apply(); + } } }); - minutesInputEl.bind('keydown', function(e) { - if ( e.which === 38 ) { // up - e.preventDefault(); - $scope.incrementMinutes(); - $scope.$apply(); + minutesInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementMinutes(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementMinutes(); + $scope.$apply(); + } } - else if ( e.which === 40 ) { // down - e.preventDefault(); - $scope.decrementMinutes(); - $scope.$apply(); + }); + + secondsInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementSeconds(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementSeconds(); + $scope.$apply(); + } } }); }; - this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { - if ( $scope.readonlyInput ) { + this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + if ($scope.readonlyInput) { $scope.updateHours = angular.noop; $scope.updateMinutes = angular.noop; + $scope.updateSeconds = angular.noop; return; } - var invalidate = function(invalidHours, invalidMinutes) { - ngModelCtrl.$setViewValue( null ); + var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { + ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', false); + } } + if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', false); + } + } + + if (angular.isDefined(invalidSeconds)) { + $scope.invalidSeconds = invalidSeconds; + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', false); + } } }; $scope.updateHours = function() { - var hours = getHoursFromTemplate(); + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(true); + } else { + refresh('h'); + } } else { invalidate(true); } }; - hoursInputEl.bind('blur', function(e) { - if ( !$scope.invalidHours && $scope.hours < 10) { - $scope.$apply( function() { - $scope.hours = pad( $scope.hours ); + hoursInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.hours === null || $scope.hours === '') { + invalidate(true); + } else if (!$scope.invalidHours && $scope.hours < 10) { + $scope.$apply(function() { + $scope.hours = pad($scope.hours, !padHours); }); } }); $scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); + + ngModelCtrl.$setDirty(); - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(undefined, true); + } else { + refresh('m'); + } } else { invalidate(undefined, true); } }; - minutesInputEl.bind('blur', function(e) { - if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + minutesInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.minutes === null) { + invalidate(undefined, true); + } else if (!$scope.invalidMinutes && $scope.minutes < 10) { + $scope.$apply(function() { + $scope.minutes = pad($scope.minutes); + }); + } + }); + + $scope.updateSeconds = function() { + var seconds = getSecondsFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(seconds)) { + selected.setSeconds(seconds); + refresh('s'); + } else { + invalidate(undefined, undefined, true); + } + }; + + secondsInputEl.on('blur', function(e) { + if (modelIsEmpty()) { + makeValid(); + } else if (!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply( function() { - $scope.minutes = pad( $scope.minutes ); + $scope.seconds = pad($scope.seconds); }); } }); @@ -3938,375 +6538,702 @@ angular.module('ui.bootstrap.timepicker', []) this.render = function() { var date = ngModelCtrl.$viewValue; - if ( isNaN(date) ) { + if (isNaN(date)) { ngModelCtrl.$setValidity('time', false); $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } else { - if ( date ) { + if (date) { selected = date; } - makeValid(); + + if (selected < min || selected > max) { + ngModelCtrl.$setValidity('time', false); + $scope.invalidHours = true; + $scope.invalidMinutes = true; + } else { + makeValid(); + } updateTemplate(); } }; // Call internally when we know that model is valid. - function refresh( keyboardChange ) { + function refresh(keyboardChange) { makeValid(); - ngModelCtrl.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); + ngModelCtrl.$setViewValue(new Date(selected)); + updateTemplate(keyboardChange); } function makeValid() { + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', true); + } + + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', true); + } + + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', true); + } + ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; + $scope.invalidSeconds = false; } - function updateTemplate( keyboardChange ) { - var hours = selected.getHours(), minutes = selected.getMinutes(); + function updateTemplate(keyboardChange) { + if (!ngModelCtrl.$modelValue) { + $scope.hours = null; + $scope.minutes = null; + $scope.seconds = null; + $scope.meridian = meridians[0]; + } else { + var hours = selected.getHours(), + minutes = selected.getMinutes(), + seconds = selected.getSeconds(); - if ( $scope.showMeridian ) { - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system - } + if ($scope.showMeridian) { + hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours); + if (keyboardChange !== 'm') { + $scope.minutes = pad(minutes); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; - $scope.hours = keyboardChange === 'h' ? hours : pad(hours); - if (keyboardChange !== 'm') { - $scope.minutes = pad(minutes); + if (keyboardChange !== 's') { + $scope.seconds = pad(seconds); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours(), dt.getMinutes() ); + function addSecondsToSelected(seconds) { + selected = addSeconds(selected, seconds); refresh(); } - + + function addMinutes(selected, minutes) { + return addSeconds(selected, minutes*60); + } + + function addSeconds(date, seconds) { + var dt = new Date(date.getTime() + seconds * 1000); + var newDate = new Date(date); + newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); + return newDate; + } + + function modelIsEmpty() { + return ($scope.hours === null || $scope.hours === '') && + ($scope.minutes === null || $scope.minutes === '') && + (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === '')); + } + $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; - + $scope.incrementHours = function() { - addMinutes( hourStep * 60 ); + if (!$scope.noIncrementHours()) { + addSecondsToSelected(hourStep * 60 * 60); + } }; + $scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); + if (!$scope.noDecrementHours()) { + addSecondsToSelected(-hourStep * 60 * 60); + } }; + $scope.incrementMinutes = function() { - addMinutes( minuteStep ); + if (!$scope.noIncrementMinutes()) { + addSecondsToSelected(minuteStep * 60); + } }; + $scope.decrementMinutes = function() { - addMinutes( - minuteStep ); + if (!$scope.noDecrementMinutes()) { + addSecondsToSelected(-minuteStep * 60); + } + }; + + $scope.incrementSeconds = function() { + if (!$scope.noIncrementSeconds()) { + addSecondsToSelected(secondStep); + } }; + + $scope.decrementSeconds = function() { + if (!$scope.noDecrementSeconds()) { + addSecondsToSelected(-secondStep); + } + }; + $scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); + + if (!$scope.noToggleMeridian()) { + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); + } else { + $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; + } + } }; + + $scope.blur = function() { + ngModelCtrl.$setTouched(); + }; + + $scope.$on('$destroy', function() { + while (watchers.length) { + watchers.shift()(); + } + }); }]) -.directive('timepicker', function () { +.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { return { - restrict: 'EA', - require: ['timepicker', '?^ngModel'], - controller:'TimepickerController', - replace: true, + require: ['uibTimepicker', '?^ngModel'], + restrict: 'A', + controller: 'UibTimepickerController', + controllerAs: 'timepicker', scope: {}, - templateUrl: 'template/timepicker/timepicker.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || uibTimepickerConfig.templateUrl; + }, link: function(scope, element, attrs, ctrls) { var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - if ( ngModelCtrl ) { - timepickerCtrl.init( ngModelCtrl, element.find('input') ); - } - } - }; -}); + if (ngModelCtrl) { + timepickerCtrl.init(ngModelCtrl, element.find('input')); + } + } + }; +}]); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('uibTypeaheadParser', ['$parse', function($parse) { + // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + return { + parse: function(input) { + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); + } + + return { + itemName: match[3], + source: $parse(match[4]), + viewMapper: $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) + }; + } + }; + }]) + + .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', + function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } + + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + minLength = !newVal && newVal !== 0 ? 1 : newVal; + }); + + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; + }; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + + //binding to a variable that indicates if there were no results after the query is completed + var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; -angular.module('ui.bootstrap.transition', []) + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; -.value('$transitionSuppressDeprecated', false) -/** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. - */ -.factory('$transition', [ - '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated', -function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) { + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - if (!$transitionSuppressDeprecated) { - $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.'); - } + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); - }; + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; - if (endEventName) { - element.bind(endEventName, transitionEndHandler); - } + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } - }); + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); + //INTERNAL VARIABLES + + //model setter executed upon match selection + var parsedModel = $parse(attrs.ngModel); + var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); + var $setModelValue = function(scope, newValue) { + if (angular.isFunction(parsedModel(originalScope)) && + ngModelOptions.getOption('getterSetter')) { + return invokeModelSetter(scope, {$$$p: newValue}); } - deferred.reject('Transition cancelled'); - }; - return deferred.promise; - }; + return parsedModel.assign(scope, newValue); + }; - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; - } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + var hasFocus; -/** - * A helper service that can parse typeahead's syntax (string provided by users) - * Extracted to a separate service for ease of unit testing - */ - .factory('typeaheadParser', ['$parse', function ($parse) { + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); - return { - parse:function (input) { + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
'); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; + //pop-up element used to display matches + var popUpEl = angular.element('
'); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); } - }; -}]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$rootScope', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $rootScope, $position, typeaheadParser) { + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; - var HOT_KEYS = [9, 13, 27, 38, 40]; + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { + var getMatchId = function(index) { + return popupId + '-option-' + index; + }; - //SUPPORTED ATTRIBUTES (OPTIONS) + // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. + // This attribute is added or removed automatically when the `activeIdx` changes. + scope.$watch('activeIdx', function(index) { + if (index < 0) { + element.removeAttr('aria-activedescendant'); + } else { + element.attr('aria-activedescendant', getMatchId(index)); + } + }); - //minimal no of characters that needs to be entered before typeahead kicks-in - var minLength = originalScope.$eval(attrs.typeaheadMinLength); - if (!minLength && minLength !== 0) { - minLength = 1; + var inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); } - //minimal wait time after last character typed before typeahead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + return false; + }; + + var getMatchesAsync = function(inputValue, evt) { + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + isNoResultsSetter(originalScope, false); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = inputValue === modelCtrl.$viewValue; + if (onCurrentRequest && hasFocus) { + if (matches && matches.length > 0) { + scope.activeIdx = focusFirst ? 0 : -1; + isNoResultsSetter(originalScope, false); + scope.matches.length = 0; + + //transform labels + for (var i = 0; i < matches.length; i++) { + locals[parserResult.itemName] = matches[i]; + scope.matches.push({ + id: getMatchId(i), + label: parserResult.viewMapper(scope, locals), + model: matches[i] + }); + } + + scope.query = inputValue; + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + recalculatePosition(); + + element.attr('aria-expanded', true); - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); + } + } - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } + } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); + } + } + if (onCurrentRequest) { + isLoadingSetter(originalScope, false); + } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + scope.moveInProgress = false; + }, eventDebounceTime); - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + // Default progress type + scope.moveInProgress = false; - //INTERNAL VARIABLES + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; + debouncedRecalculate(); + } - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); + // recalculate actual position and set new values to scope + // after digest loop is popup in right position + function recalculatePosition() { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top += element.prop('offsetHeight'); + } - var hasFocus; + //we need to propagate user's query so we can higlight matches + scope.query = undefined; - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; - //pop-up element used to display matches - var popUpEl = angular.element('
'); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); } + }; - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - }; + resetMatches(); - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; - // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. - // This attribute is added or removed automatically when the `activeIdx` changes. - scope.$watch('activeIdx', function(index) { - if (index < 0) { - element.removeAttr('aria-activedescendant'); - } else { - element.attr('aria-activedescendant', getMatchId(index)); - } + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals), + $event: evt }); - var getMatchesAsync = function(inputValue) { + resetMatches(); - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { + $timeout(function() { element[0].focus(); }, 0, false); + } + }; - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - var onCurrentRequest = (inputValue === modelCtrl.$viewValue); - if (onCurrentRequest && hasFocus) { - if (matches && matches.length > 0) { + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } - scope.activeIdx = focusFirst ? 0 : -1; - scope.matches.length = 0; + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); - //transform labels - for(var i=0; i 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(scope.activeIdx, evt); + } + }); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); + } + }); + + element.on('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.on('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); } - }, function(){ - resetMatches(); - isLoadingSetter(originalScope, false); }); - }; + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + hasFocus = false; + selected = false; + }); - resetMatches(); + // Keep reference to click handler to unbind it. + var dismissClickHandler = function(evt) { + // Issue #3973 + // Firefox treats right click as a click on document + if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { + resetMatches(); + if (!$rootScope.$$phase) { + originalScope.$digest(); + } + } + }; - //we need to propagate user's query so we can higlight matches - scope.query = undefined; + $document.on('click', dismissClickHandler); - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var timeoutPromise; + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } - var scheduleSearchWithTimeout = function(inputValue) { - timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); - }; + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); - var cancelPreviousTimeout = function() { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise); - } - }; + if (showHint) { + inputsContainer.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + + this.init = function(_modelCtrl) { + modelCtrl = _modelCtrl; + ngModelOptions = extractOptions(modelCtrl); + + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue - modelCtrl.$parsers.unshift(function (inputValue) { - + modelCtrl.$parsers.unshift(function(inputValue) { hasFocus = true; if (minLength === 0 || inputValue && inputValue.length >= minLength) { @@ -4324,20 +7251,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap if (isEditable) { return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } } - }); - modelCtrl.$formatters.push(function (modelValue) { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } + + modelCtrl.$setValidity('editable', false); + return undefined; + }); + modelCtrl.$formatters.push(function(modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; @@ -4349,255 +7275,212 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } if (inputFormatter) { - locals.$model = modelValue; return inputFormatter(originalScope, locals); - - } else { - - //it might happen that we don't have enough info to properly render input value - //we need to check for this situation and simply return model value if we can't apply custom formatting - locals[parserResult.itemName] = modelValue; - candidateViewValue = parserResult.viewMapper(originalScope, locals); - locals[parserResult.itemName] = undefined; - emptyViewValue = parserResult.viewMapper(originalScope, locals); - - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } - }); - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(originalScope, locals) - }); - - resetMatches(); - - //return focus to the input element if a match was selected via a mouse click event - // use timeout to avoid $rootScope:inprog error - $timeout(function() { element[0].focus(); }, 0, false); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } - - // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything - if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { - return; } - evt.preventDefault(); - - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } - }); + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); - element.bind('blur', function (evt) { - hasFocus = false; + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); + }; - // Keep reference to click handler to unbind it. - var dismissClickHandler = function (evt) { - if (element[0] !== evt.target) { - resetMatches(); - if (!$rootScope.$$phase) { - scope.$digest(); - } - } - }; - - $document.bind('click', dismissClickHandler); - - originalScope.$on('$destroy', function(){ - $document.unbind('click', dismissClickHandler); - if (appendToBody) { - $popup.remove(); - } - // Prevent jQuery cache memory leak - popUpEl.remove(); - }); + function extractOptions(ngModelCtrl) { + var ngModelOptions; - var $popup = $compile(popUpEl)(scope); + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; - if (appendToBody) { - $document.find('body').append($popup); - } else { - element.after($popup); + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; } + + return ngModelOptions; } - }; + }]) -}]) + .directive('uibTypeahead', function() { + return { + controller: 'UibTypeaheadController', + require: ['ngModel', 'uibTypeahead'], + link: function(originalScope, element, attrs, ctrls) { + ctrls[1].init(ctrls[0]); + } + }; + }) - .directive('typeaheadPopup', function () { + .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) { return { - restrict:'EA', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'&', - select:'&' + scope: { + matches: '=', + query: '=', + active: '=', + position: '&', + moveInProgress: '=', + select: '&', + assignIsOpen: '&', + debounce: '&' }, - replace:true, - templateUrl:'template/typeahead/typeahead-popup.html', - link:function (scope, element, attrs) { - + replace: true, + templateUrl: function(element, attrs) { + return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; + }, + link: function(scope, element, attrs) { scope.templateUrl = attrs.templateUrl; - scope.isOpen = function () { - return scope.matches.length > 0; + scope.isOpen = function() { + var isDropdownOpen = scope.matches.length > 0; + scope.assignIsOpen({ isOpen: isDropdownOpen }); + return isDropdownOpen; }; - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; + scope.isActive = function(matchIdx) { + return scope.active === matchIdx; }; - scope.selectActive = function (matchIdx) { + scope.selectActive = function(matchIdx) { scope.active = matchIdx; }; - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); + scope.selectMatch = function(activeIdx, evt) { + var debounce = scope.debounce(); + if (angular.isNumber(debounce) || angular.isObject(debounce)) { + $$debounce(function() { + scope.select({activeIdx: activeIdx, evt: evt}); + }, angular.isNumber(debounce) ? debounce : debounce['default']); + } else { + scope.select({activeIdx: activeIdx, evt: evt}); + } }; } }; - }) + }]) - .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) { + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { return { - restrict:'EA', - scope:{ - index:'=', - match:'=', - query:'=' + scope: { + index: '=', + match: '=', + query: '=' }, - link:function (scope, element, attrs) { - var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + link: function(scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; $templateRequest(tplUrl).then(function(tplContent) { - $compile(tplContent.trim())(scope, function(clonedElement){ - element.replaceWith(clonedElement); - }); + var tplEl = angular.element(tplContent.trim()); + element.replaceWith(tplEl); + $compile(tplEl)(scope); }); } }; }]) - .filter('typeaheadHighlight', function() { + .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if (!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if (!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); + }]); -angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion-group.html", - "
\n" + - "
\n" + - "

\n" + - " {{heading}}\n" + - "

\n" + - "
\n" + - "
\n" + - "
\n" + - "
\n" + +angular.module("uib/template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion-group.html", + "
\n" + + "

\n" + + " {{heading}}\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + "
\n" + ""); }]); -angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion.html", - "
"); +angular.module("uib/template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion.html", + "
"); }]); -angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/alert/alert.html", - "
\n" + - " \n" + - "
\n" + - "
\n" + +angular.module("uib/template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/alert/alert.html", + "\n" + + "
\n" + ""); }]); -angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/carousel.html", - "
\n" + - "
    1\">\n" + - "
  1. \n" + - "
\n" + - "
\n" + - " 1\">\n" + - " 1\">\n" + - "
\n" + +angular.module("uib/template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/carousel.html", + "
\n" + + " 1\">\n" + + " \n" + + " previous\n" + + "\n" + + " 1\">\n" + + " \n" + + " next\n" + + "\n" + + "
    1\">\n" + + "
  1. \n" + + " slide {{ $index + 1 }} of {{ slides.length }}, currently active\n" + + "
  2. \n" + + "
\n" + ""); }]); -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/slide.html", - "
\n" + +angular.module("uib/template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/slide.html", + "
\n" + ""); }]); -angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/datepicker.html", - "
\n" + - " \n" + - " \n" + - " \n" + - "
"); +angular.module("uib/template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/datepicker.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); }]); -angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/day.html", +angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/day.html", "\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + @@ -4605,10 +7488,19 @@ angular.module("template/datepicker/day.html", []).run(["$templateCache", functi " \n" + " \n" + " \n" + - " \n" + + " \n" + " \n" + - " \n" + " \n" + " \n" + @@ -4616,20 +7508,29 @@ angular.module("template/datepicker/day.html", []).run(["$templateCache", functi ""); }]); -angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/month.html", +angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/month.html", "
{{ weekNumbers[$index] }}\n" + - " \n" + + " \n" + + " \n" + "
\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + + " \n" + " \n" + " \n" + @@ -4637,35 +7538,29 @@ angular.module("template/datepicker/month.html", []).run(["$templateCache", func ""); }]); -angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/popup.html", - "\n" + - ""); -}]); - -angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/year.html", +angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/year.html", "
\n" + - " \n" + + "
\n" + + " \n" + "
\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + + " \n" + " \n" + " \n" + @@ -4673,204 +7568,185 @@ angular.module("template/datepicker/year.html", []).run(["$templateCache", funct ""); }]); -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
\n" + +angular.module("uib/template/datepickerPopup/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepickerPopup/popup.html", + "\n" + ""); }]); -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
\n" + - "
\n" + - "
\n" + +angular.module("uib/template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/modal/window.html", + "
\n" + ""); }]); -angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pager.html", - ""); +angular.module("uib/template/pager/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pager/pager.html", + "
  • {{::getText('previous')}}
  • \n" + + "
  • {{::getText('next')}}
  • \n" + + ""); }]); -angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pagination.html", - ""); +angular.module("uib/template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pagination/pagination.html", + "
  • {{::getText('first')}}
  • \n" + + "
  • {{::getText('previous')}}
  • \n" + + "
  • {{page.text}}
  • \n" + + "
  • {{::getText('next')}}
  • \n" + + "
  • {{::getText('last')}}
  • \n" + + ""); }]); -angular.module("template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-html-popup.html", - "
    \n" + - "
    \n" + - "
    \n" + - "
    \n" + +angular.module("uib/template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-html-popup.html", + "
    \n" + + "
    \n" + ""); }]); -angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", - "
    \n" + - "
    \n" + - "
    \n" + - "
    \n" + +angular.module("uib/template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-popup.html", + "
    \n" + + "
    \n" + ""); }]); -angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-popup.html", - "
    \n" + - "
    \n" + - "
    \n" + - "
    \n" + +angular.module("uib/template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-template-popup.html", + "
    \n" + + "
    \n" + ""); }]); -angular.module("template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-template-popup.html", - "
    \n" + - "
    \n" + - "
    \n" + +angular.module("uib/template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover-html.html", + "
    \n" + + "\n" + + "
    \n" + + "

    \n" + + "
    \n" + "
    \n" + ""); }]); -angular.module("template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/popover/popover-template.html", - "
    \n" + - "
    \n" + +angular.module("uib/template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover-template.html", + "
    \n" + "\n" + - "
    \n" + - "

    \n" + - "
    \n" + - "
    \n" + + "
    \n" + + "

    \n" + + "
    \n" + "
    \n" + ""); }]); -angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/popover/popover.html", - "
    \n" + - "
    \n" + +angular.module("uib/template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover.html", + "
    \n" + "\n" + - "
    \n" + - "

    \n" + - "
    \n" + - "
    \n" + + "
    \n" + + "

    \n" + + "
    \n" + "
    \n" + ""); }]); -angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/bar.html", - "
    \n" + +angular.module("uib/template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/bar.html", + "
    \n" + ""); }]); -angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progress.html", - "
    "); +angular.module("uib/template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progress.html", + "
    "); }]); -angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progressbar.html", +angular.module("uib/template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progressbar.html", "
    \n" + - "
    \n" + + "
    \n" + "
    \n" + ""); }]); -angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/rating/rating.html", - "\n" + - " \n" + - " ({{ $index < value ? '*' : ' ' }})\n" + - " \n" + - ""); +angular.module("uib/template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/rating/rating.html", + "\n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + "\n" + + ""); }]); -angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tab.html", - "
  • \n" + - " {{heading}}\n" + +angular.module("uib/template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + "
  • \n" + ""); }]); -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", +angular.module("uib/template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tabset.html", "
    \n" + - "
      \n" + + "
        \n" + "
        \n" + - "
        \n" + + "
        \n" + "
        \n" + "
        \n" + "
        \n" + ""); }]); -angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/timepicker/timepicker.html", - "
        \n" + - " \n" + + "
        \n" + + " \n" + "
        \n" + +angular.module("uib/template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/timepicker/timepicker.html", + "
        \n" + " \n" + - " \n" + - " \n" + + " \n" + + " \n" + " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + - " \n" + + " \n" + " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + @@ -4878,18 +7754,28 @@ angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", ""); }]); -angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-match.html", - ""); +angular.module("uib/template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/typeahead/typeahead-match.html", + "\n" + + ""); }]); -angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-popup.html", - "
          
        \n" + - " \n" + + " \n" + + " \n" + " :\n" + - " \n" + + " :\n" + + " \n" + " :\n" + + " \n" + + "
          
        \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
        {{::label.abbr}}
        {{ weekNumbers[$index] }}\n \n
        \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'\n'); - -}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'\n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/tooltip/tooltip-template-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-template-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/popover/popover-template.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover-template.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
        \n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
        ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
        \n
        \n
        \n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
      • \n {{heading}}\n
      • \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
        \n \n
        \n
        \n
        \n
        \n
        \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
         
        \n \n :\n \n
         
        \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'\n')}]),!angular.$$csp()&&angular.element(document).find("head").prepend(''); \ No newline at end of file + */angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$q","$parse","$injector",function(e,t,n,a){var i=a.has("$animateCss")?a.get("$animateCss"):null;return{link:function(a,o,r){function l(){b=!!("horizontal"in r),b?(v={width:""},$={width:"0"}):(v={height:""},$={height:"0"}),a.$eval(r.uibCollapse)||o.addClass("in").addClass("collapse").attr("aria-expanded",!0).attr("aria-hidden",!1).css(v)}function s(e){return b?{width:e.scrollWidth+"px"}:{height:e.scrollHeight+"px"}}function u(){o.hasClass("collapse")&&o.hasClass("in")||t.resolve(m(a)).then(function(){o.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),i?i(o,{addClass:"in",easing:"ease",css:{overflow:"hidden"},to:s(o[0])}).start()["finally"](p):e.addClass(o,"in",{css:{overflow:"hidden"},to:s(o[0])}).then(p)},angular.noop)}function p(){o.removeClass("collapsing").addClass("collapse").css(v),f(a)}function c(){return o.hasClass("collapse")||o.hasClass("in")?void t.resolve(h(a)).then(function(){o.css(s(o[0])).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),i?i(o,{removeClass:"in",to:$}).start()["finally"](d):e.removeClass(o,"in",{to:$}).then(d)},angular.noop):d()}function d(){o.css($),o.removeClass("collapsing").addClass("collapse"),g(a)}var m=n(r.expanding),f=n(r.expanded),h=n(r.collapsing),g=n(r.collapsed),b=!1,v={},$={};l(),a.$watch(r.uibCollapse,function(e){e?c():u()})}}}]),angular.module("ui.bootstrap.tabindex",[]).directive("uibTabindexToggle",function(){return{restrict:"A",link:function(e,t,n){n.$observe("disabled",function(e){n.$set("tabindex",e?-1:null)})}}}),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse","ui.bootstrap.tabindex"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(e,t,n){this.groups=[],this.closeOthers=function(a){var i=angular.isDefined(t.closeOthers)?e.$eval(t.closeOthers):n.closeOthers;i&&angular.forEach(this.groups,function(e){e!==a&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(n){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(e,t){return t.templateUrl||"uib/template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,restrict:"A",templateUrl:function(e,t){return t.templateUrl||"uib/template/accordion/accordion-group.html"},scope:{heading:"@",panelClass:"@?",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(e,t,n,a){t.addClass("panel"),a.addGroup(e),e.openClass=n.openClass||"panel-open",e.panelClass=n.panelClass||"panel-default",e.$watch("isOpen",function(n){t.toggleClass(e.openClass,!!n),n&&a.closeOthers(e)}),e.toggleOpen=function(t){e.isDisabled||t&&32!==t.which||(e.isOpen=!e.isOpen)};var i="accordiongroup-"+e.$id+"-"+Math.floor(1e4*Math.random());e.headingId=i+"-tab",e.panelId=i+"-panel"}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(e,t,n,a,i){a.setHeading(i(e,angular.noop))}}}).directive("uibAccordionTransclude",function(){function e(){return"uib-accordion-header,data-uib-accordion-header,x-uib-accordion-header,uib\\:accordion-header,[uib-accordion-header],[data-uib-accordion-header],[x-uib-accordion-header]"}return{require:"^uibAccordionGroup",link:function(t,n,a,i){t.$watch(function(){return i[a.uibAccordionTransclude]},function(t){if(t){var a=angular.element(n[0].querySelector(e()));a.html(""),a.append(t)}})}}}),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$element","$attrs","$interpolate","$timeout",function(e,t,n,a,i){e.closeable=!!n.close,t.addClass("alert"),n.$set("role","alert"),e.closeable&&t.addClass("alert-dismissible");var o=angular.isDefined(n.dismissOnTimeout)?a(n.dismissOnTimeout)(e.$parent):null;o&&i(function(){e.close()},parseInt(o,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",restrict:"A",templateUrl:function(e,t){return t.templateUrl||"uib/template/alert/alert.html"},transclude:!0,scope:{close:"&"}}}),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("uibBtnRadio",["$parse",function(e){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(t,n,a,i){var o=i[0],r=i[1],l=e(a.uibUncheckable);n.find("input").css({display:"none"}),r.$render=function(){n.toggleClass(o.activeClass,angular.equals(r.$modelValue,t.$eval(a.uibBtnRadio)))},n.on(o.toggleEvent,function(){if(!a.disabled){var e=n.hasClass(o.activeClass);(!e||angular.isDefined(a.uncheckable))&&t.$apply(function(){r.$setViewValue(e?null:t.$eval(a.uibBtnRadio)),r.$render()})}}),a.uibUncheckable&&t.$watch(l,function(e){a.$set("uncheckable",e?"":void 0)})}}}]).directive("uibBtnCheckbox",function(){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(e,t,n,a){function i(){return r(n.btnCheckboxTrue,!0)}function o(){return r(n.btnCheckboxFalse,!1)}function r(t,n){return angular.isDefined(t)?e.$eval(t):n}var l=a[0],s=a[1];t.find("input").css({display:"none"}),s.$render=function(){t.toggleClass(l.activeClass,angular.equals(s.$modelValue,i()))},t.on(l.toggleEvent,function(){n.disabled||e.$apply(function(){s.$setViewValue(t.hasClass(l.activeClass)?o():i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$timeout","$animate",function(e,t,n,a,i){function o(e){for(var t=0,n=g.length;n>t;++t)if(g[t].index===e)return g[t]}function r(e){for(var t=0;t1){g[a].element.data(b,n.direction);var s=h.getCurrentIndex();angular.isNumber(s)&&g[s].element&&g[s].element.data(b,n.direction),e.$currentTransition=!0,i.on("addClass",g[a].element,function(t,n){"close"===n&&(e.$currentTransition=null,i.off("addClass",t))})}e.active=n.index,v=n.index,r(a),e.$evalAsync(function(){angular.extend(o(h.getCurrentIndex()),{fetch:!0})}),c()}}function s(e){for(var t=0;t0&&(m=n(d,t))}function d(){var t=+e.interval;f&&!isNaN(t)&&t>0&&g.length?e.next():e.pause()}var m,f,h=this,g=h.slides=e.slides=[],b="uib-slideDirection",v=e.active,$=!1;t.addClass("carousel"),h.addSlide=function(t,n){g.push({slide:t,element:n}),g.sort(function(e,t){return+e.slide.index-+t.slide.index}),(t.index===e.active||1===g.length&&!angular.isNumber(e.active))&&(e.$currentTransition&&(e.$currentTransition=null),v=t.index,e.active=t.index,r(v),h.select(g[s(t)]),1===g.length&&e.play())},h.getCurrentIndex=function(){for(var e=0;e0&&v===n?n>=g.length?(v=g.length-1,e.active=v,r(v),h.select(g[g.length-1])):(v=n,e.active=v,r(v),h.select(g[n])):v>n&&(v--,e.active=v),0===g.length&&(v=null,e.active=null)},h.select=e.select=function(t,n){var a=s(t.slide);void 0===n&&(n=a>h.getCurrentIndex()?"next":"prev"),t.slide.index===v||e.$currentTransition||l(t.slide,a,n)},e.indexOfSlide=function(e){return+e.slide.index},e.isActive=function(t){return e.active===t.slide.index},e.isPrevDisabled=function(){return 0===e.active&&e.noWrap()},e.isNextDisabled=function(){return e.active===g.length-1&&e.noWrap()},e.pause=function(){e.noPause||(f=!1,u())},e.play=function(){f||(f=!0,c())},t.on("mouseenter",e.pause),t.on("mouseleave",e.play),e.$on("$destroy",function(){$=!0,u()}),e.$watch("noTransition",function(e){i.enabled(t,!e)}),e.$watch("interval",c),e.$watchCollection("slides",p),e.$watch("active",function(e){if(angular.isNumber(e)&&v!==e){for(var t=0;t-1){var o=!1;e=e.split("");for(var r=i;r-1){e=e.split(""),n[i]="("+a.regex+")",e[i]="$";for(var o=i+1,r=i+a.key.length;r>o;o++)n[o]="",e[o]="$";e=e.join(""),t.push({index:i,key:a.key,apply:a.apply,matcher:a.regex})}}),{regex:new RegExp("^"+n.join("")+"$"),map:a(t,"index")}}function l(e){for(var t,n,a=[],i=0;i=e.length||"'"!==e.charAt(i+1))&&(a.push(s(e,n,i)),n=null);else if(i===e.length)for(;nn?!1:1===t&&n>28?29===n&&(e%4===0&&e%100!==0||e%400===0):3===t||5===t||8===t||10===t?31>n:!0}function c(e){return parseInt(e,10)}function d(e,t){return e&&t?g(e,t):e}function m(e,t){return e&&t?g(e,t,!0):e}function f(e,t){e=e.replace(/:/g,"");var n=Date.parse("Jan 01, 1970 00:00:00 "+e)/6e4;return isNaN(n)?t:n}function h(e,t){return e=new Date(e.getTime()),e.setMinutes(e.getMinutes()+t),e}function g(e,t,n){n=n?-1:1;var a=e.getTimezoneOffset(),i=f(t,a);return h(e,n*(i-a))}var b,v,$=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){b=t.id,this.parsers={},this.formatters={},v=[{key:"yyyy",regex:"\\d{4}",apply:function(e){this.year=+e},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"yyyy")}},{key:"yy",regex:"\\d{2}",apply:function(e){e=+e,this.year=69>e?e+2e3:e+1900},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"yy")}},{key:"y",regex:"\\d{1,4}",apply:function(e){this.year=+e},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"y")}},{key:"M!",regex:"0?[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){var t=e.getMonth();return/^[0-9]$/.test(t)?n(e,"MM"):n(e,"M")}},{key:"MMMM",regex:t.DATETIME_FORMATS.MONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.MONTH.indexOf(e)},formatter:function(e){return n(e,"MMMM")}},{key:"MMM",regex:t.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.SHORTMONTH.indexOf(e)},formatter:function(e){return n(e,"MMM")}},{key:"MM",regex:"0[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){return n(e,"MM")}},{key:"M",regex:"[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){return n(e,"M")}},{key:"d!",regex:"[0-2]?[0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){var t=e.getDate();return/^[1-9]$/.test(t)?n(e,"dd"):n(e,"d")}},{key:"dd",regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){return n(e,"dd")}},{key:"d",regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){return n(e,"d")}},{key:"EEEE",regex:t.DATETIME_FORMATS.DAY.join("|"),formatter:function(e){return n(e,"EEEE")}},{key:"EEE",regex:t.DATETIME_FORMATS.SHORTDAY.join("|"),formatter:function(e){return n(e,"EEE")}},{key:"HH",regex:"(?:0|1)[0-9]|2[0-3]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"HH")}},{key:"hh",regex:"0[0-9]|1[0-2]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"hh")}},{key:"H",regex:"1?[0-9]|2[0-3]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"H")}},{key:"h",regex:"[0-9]|1[0-2]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"h")}},{key:"mm",regex:"[0-5][0-9]",apply:function(e){this.minutes=+e},formatter:function(e){return n(e,"mm")}},{key:"m",regex:"[0-9]|[1-5][0-9]",apply:function(e){this.minutes=+e},formatter:function(e){return n(e,"m")}},{key:"sss",regex:"[0-9][0-9][0-9]",apply:function(e){this.milliseconds=+e},formatter:function(e){return n(e,"sss")}},{key:"ss",regex:"[0-5][0-9]",apply:function(e){this.seconds=+e},formatter:function(e){return n(e,"ss")}},{key:"s",regex:"[0-9]|[1-5][0-9]",apply:function(e){this.seconds=+e},formatter:function(e){return n(e,"s")}},{key:"a",regex:t.DATETIME_FORMATS.AMPMS.join("|"),apply:function(e){12===this.hours&&(this.hours=0),"PM"===e&&(this.hours+=12)},formatter:function(e){return n(e,"a")}},{key:"Z",regex:"[+-]\\d{4}",apply:function(e){var t=e.match(/([+-])(\d{2})(\d{2})/),n=t[1],a=t[2],i=t[3];this.hours+=c(n+a),this.minutes+=c(n+i)},formatter:function(e){return n(e,"Z")}},{key:"ww",regex:"[0-4][0-9]|5[0-3]",formatter:function(e){return n(e,"ww")}},{key:"w",regex:"[0-9]|[1-4][0-9]|5[0-3]",formatter:function(e){return n(e,"w")}},{key:"GGGG",regex:t.DATETIME_FORMATS.ERANAMES.join("|").replace(/\s/g,"\\s"),formatter:function(e){return n(e,"GGGG")}},{key:"GGG",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"GGG")}},{key:"GG",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"GG")}},{key:"G",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"G")}}],angular.version.major>=1&&angular.version.minor>4&&v.push({key:"LLLL",regex:t.DATETIME_FORMATS.STANDALONEMONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.STANDALONEMONTH.indexOf(e)},formatter:function(e){return n(e,"LLLL")}})},this.init(),this.getParser=function(e){var t=o(e);return t&&t.apply||null},this.overrideParser=function(e,t){var n=o(e);n&&angular.isFunction(t)&&(this.parsers={},n.apply=t)}.bind(this),this.filter=function(e,n){if(!angular.isDate(e)||isNaN(e)||!n)return"";n=t.DATETIME_FORMATS[n]||n,t.id!==b&&this.init(),this.formatters[n]||(this.formatters[n]=l(n));var a=this.formatters[n];return a.reduce(function(t,n){return t+n(e)},"")},this.parse=function(n,a,i){if(!angular.isString(n)||!a)return n;a=t.DATETIME_FORMATS[a]||a,a=a.replace($,"\\$&"),t.id!==b&&this.init(),this.parsers[a]||(this.parsers[a]=r(a,"apply"));var o=this.parsers[a],l=o.regex,s=o.map,u=n.match(l),c=!1;if(u&&u.length){var d,m;angular.isDate(i)&&!isNaN(i.getTime())?d={year:i.getFullYear(),month:i.getMonth(),date:i.getDate(),hours:i.getHours(),minutes:i.getMinutes(),seconds:i.getSeconds(),milliseconds:i.getMilliseconds()}:(i&&e.warn("dateparser:","baseDate is not a valid date"),d={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var f=1,h=u.length;h>f;f++){var g=s[f-1];"Z"===g.matcher&&(c=!0),g.apply&&g.apply.call(d,u[f])}var v=c?Date.prototype.setUTCFullYear:Date.prototype.setFullYear,y=c?Date.prototype.setUTCHours:Date.prototype.setHours;return p(d.year,d.month,d.date)&&(!angular.isDate(i)||isNaN(i.getTime())||c?(m=new Date(0),v.call(m,d.year,d.month,d.date),y.call(m,d.hours||0,d.minutes||0,d.seconds||0,d.milliseconds||0)):(m=new Date(i),v.call(m,d.year,d.month,d.date),y.call(m,d.hours,d.minutes,d.seconds,d.milliseconds))),m}},this.toTimezone=d,this.fromTimezone=m,this.timezoneToOffset=f,this.addDateMinutes=h,this.convertTimezoneToLocal=g}]),angular.module("ui.bootstrap.isClass",[]).directive("uibIsClass",["$animate",function(e){var t=/^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/,n=/^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;return{restrict:"A",compile:function(a,i){function o(e,t,n){s.push(e),u.push({scope:e,element:t}),f.forEach(function(t,n){r(t,e)}),e.$on("$destroy",l)}function r(t,a){var i=t.match(n),o=a.$eval(i[1]),r=i[2],l=p[t];if(!l){var s=function(t){var n=null;u.some(function(e){var a=e.scope.$eval(d);return a===t?(n=e,!0):void 0}),l.lastActivated!==n&&(l.lastActivated&&e.removeClass(l.lastActivated.element,o),n&&e.addClass(n.element,o),l.lastActivated=n)};p[t]=l={lastActivated:null,scope:a,watchFn:s,compareWithExp:r,watcher:a.$watch(r,s)}}l.watchFn(a.$eval(r))}function l(e){var t=e.targetScope,n=s.indexOf(t);if(s.splice(n,1),u.splice(n,1),s.length){var a=s[0];angular.forEach(p,function(e){e.scope===t&&(e.watcher=a.$watch(e.compareWithExp,e.watchFn),e.scope=a)})}else p={}}var s=[],u=[],p={},c=i.uibIsClass.match(t),d=c[2],m=c[1],f=m.split(",");return o}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.isClass"]).value("$datepickerSuppressError",!1).value("$datepickerLiteralWarning",!0).constant("uibDatepickerConfig",{datepickerMode:"day",formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",maxDate:null,maxMode:"year",minDate:null,minMode:"day",monthColumns:3,ngModelOptions:{},shortcutPropagation:!1,showWeeks:!0,yearColumns:5,yearRows:4}).controller("UibDatepickerController",["$scope","$element","$attrs","$parse","$interpolate","$locale","$log","dateFilter","uibDatepickerConfig","$datepickerLiteralWarning","$datepickerSuppressError","uibDateParser",function(e,t,n,a,i,o,r,l,s,u,p,c){function d(t){e.datepickerMode=t,e.datepickerOptions.datepickerMode=t}function m(t){var n;if(angular.version.minor<6)n=t.$options||e.datepickerOptions.ngModelOptions||s.ngModelOptions||{},n.getOption=function(e){return n[e]};else{var a=t.$options.getOption("timezone")||(e.datepickerOptions.ngModelOptions?e.datepickerOptions.ngModelOptions.timezone:null)||(s.ngModelOptions?s.ngModelOptions.timezone:null);n=t.$options.createChild(s.ngModelOptions).createChild(e.datepickerOptions.ngModelOptions).createChild(t.$options).createChild({timezone:a})}return n}var f=this,h={$setViewValue:angular.noop},g={},b=[];t.addClass("uib-datepicker"),n.$set("role","application"),e.datepickerOptions||(e.datepickerOptions={}),this.modes=["day","month","year"],["customClass","dateDisabled","datepickerMode","formatDay","formatDayHeader","formatDayTitle","formatMonth","formatMonthTitle","formatYear","maxDate","maxMode","minDate","minMode","monthColumns","showWeeks","shortcutPropagation","startingDay","yearColumns","yearRows"].forEach(function(t){switch(t){case"customClass":case"dateDisabled":e[t]=e.datepickerOptions[t]||angular.noop;break;case"datepickerMode":e.datepickerMode=angular.isDefined(e.datepickerOptions.datepickerMode)?e.datepickerOptions.datepickerMode:s.datepickerMode;break;case"formatDay":case"formatDayHeader":case"formatDayTitle":case"formatMonth":case"formatMonthTitle":case"formatYear":f[t]=angular.isDefined(e.datepickerOptions[t])?i(e.datepickerOptions[t])(e.$parent):s[t];break;case"monthColumns":case"showWeeks":case"shortcutPropagation":case"yearColumns":case"yearRows":f[t]=angular.isDefined(e.datepickerOptions[t])?e.datepickerOptions[t]:s[t];break;case"startingDay":angular.isDefined(e.datepickerOptions.startingDay)?f.startingDay=e.datepickerOptions.startingDay:angular.isNumber(s.startingDay)?f.startingDay=s.startingDay:f.startingDay=(o.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7;break;case"maxDate":case"minDate":e.$watch("datepickerOptions."+t,function(e){e?angular.isDate(e)?f[t]=c.fromTimezone(new Date(e),g.getOption("timezone")):(u&&r.warn("Literal date support has been deprecated, please switch to date object usage"),f[t]=new Date(l(e,"medium"))):f[t]=s[t]?c.fromTimezone(new Date(s[t]),g.getOption("timezone")):null,f.refreshView()});break;case"maxMode":case"minMode":e.datepickerOptions[t]?e.$watch(function(){return e.datepickerOptions[t]},function(n){f[t]=e[t]=angular.isDefined(n)?n:e.datepickerOptions[t],("minMode"===t&&f.modes.indexOf(e.datepickerOptions.datepickerMode)f.modes.indexOf(f[t]))&&(e.datepickerMode=f[t],e.datepickerOptions.datepickerMode=f[t])}):f[t]=e[t]=s[t]||null}}),e.uniqueId="datepicker-"+e.$id+"-"+Math.floor(1e4*Math.random()),e.disabled=angular.isDefined(n.disabled)||!1,angular.isDefined(n.ngDisabled)&&b.push(e.$parent.$watch(n.ngDisabled,function(t){e.disabled=t,f.refreshView()})),e.isActive=function(t){return 0===f.compare(t.date,f.activeDate)?(e.activeDateId=t.uid,!0):!1},this.init=function(t){h=t,g=m(h),e.datepickerOptions.initDate?(f.activeDate=c.fromTimezone(e.datepickerOptions.initDate,g.getOption("timezone"))||new Date,e.$watch("datepickerOptions.initDate",function(e){e&&(h.$isEmpty(h.$modelValue)||h.$invalid)&&(f.activeDate=c.fromTimezone(e,g.getOption("timezone")),f.refreshView())})):f.activeDate=new Date;var n=h.$modelValue?new Date(h.$modelValue):new Date;this.activeDate=isNaN(n)?c.fromTimezone(new Date,g.getOption("timezone")):c.fromTimezone(n,g.getOption("timezone")),h.$render=function(){f.render()}},this.render=function(){if(h.$viewValue){var e=new Date(h.$viewValue),t=!isNaN(e);t?this.activeDate=c.fromTimezone(e,g.getOption("timezone")):p||r.error('Datepicker directive: "ng-model" value must be a Date object')}this.refreshView()},this.refreshView=function(){if(this.element){e.selectedDt=null,this._refreshView(),e.activeDt&&(e.activeDateId=e.activeDt.uid);var t=h.$viewValue?new Date(h.$viewValue):null;t=c.fromTimezone(t,g.getOption("timezone")),h.$setValidity("dateDisabled",!t||this.element&&!this.isDisabled(t))}},this.createDateObject=function(t,n){var a=h.$viewValue?new Date(h.$viewValue):null;a=c.fromTimezone(a,g.getOption("timezone"));var i=new Date;i=c.fromTimezone(i,g.getOption("timezone"));var o=this.compare(t,i),r={date:t,label:c.filter(t,n),selected:a&&0===this.compare(t,a),disabled:this.isDisabled(t),past:0>o,current:0===o,future:o>0,customClass:this.customClass(t)||null};return a&&0===this.compare(t,a)&&(e.selectedDt=r),f.activeDate&&0===this.compare(r.date,f.activeDate)&&(e.activeDt=r),r},this.isDisabled=function(t){return e.disabled||this.minDate&&this.compare(t,this.minDate)<0||this.maxDate&&this.compare(t,this.maxDate)>0||e.dateDisabled&&e.dateDisabled({date:t,mode:e.datepickerMode})},this.customClass=function(t){return e.customClass({date:t,mode:e.datepickerMode})},this.split=function(e,t){for(var n=[];e.length>0;)n.push(e.splice(0,t));return n},e.select=function(t){if(e.datepickerMode===f.minMode){var n=h.$viewValue?c.fromTimezone(new Date(h.$viewValue),g.getOption("timezone")):new Date(0,0,0,0,0,0,0);n.setFullYear(t.getFullYear(),t.getMonth(),t.getDate()),n=c.toTimezone(n,g.getOption("timezone")),h.$setViewValue(n),h.$render()}else f.activeDate=t,d(f.modes[f.modes.indexOf(e.datepickerMode)-1]),e.$emit("uib:datepicker.mode");e.$broadcast("uib:datepicker.focus")},e.move=function(e){var t=f.activeDate.getFullYear()+e*(f.step.years||0),n=f.activeDate.getMonth()+e*(f.step.months||0);f.activeDate.setFullYear(t,n,1),f.refreshView()},e.toggleMode=function(t){t=t||1,e.datepickerMode===f.maxMode&&1===t||e.datepickerMode===f.minMode&&-1===t||(d(f.modes[f.modes.indexOf(e.datepickerMode)+t]),e.$emit("uib:datepicker.mode"))},e.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var v=function(){f.element[0].focus()};e.$on("uib:datepicker.focus",v),e.keydown=function(t){var n=e.keys[t.which];if(n&&!t.shiftKey&&!t.altKey&&!e.disabled)if(t.preventDefault(),f.shortcutPropagation||t.stopPropagation(),"enter"===n||"space"===n){if(f.isDisabled(f.activeDate))return;e.select(f.activeDate)}else!t.ctrlKey||"up"!==n&&"down"!==n?(f.handleKeyDown(n,t),f.refreshView()):e.toggleMode("up"===n?1:-1)},t.on("keydown",function(t){e.$apply(function(){e.keydown(t)})}),e.$on("$destroy",function(){for(;b.length;)b.shift()()})}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(e,t,n){function a(e,t){return 1!==t||e%4!==0||e%100===0&&e%400!==0?o[t]:29}function i(e){var t=new Date(e);t.setDate(t.getDate()+4-(t.getDay()||7));var n=t.getTime();return t.setMonth(0),t.setDate(1),Math.floor(Math.round((n-t)/864e5)/7)+1}var o=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=t,this.init=function(t){angular.extend(t,this),e.showWeeks=t.showWeeks,t.refreshView()},this.getDates=function(e,t){for(var n,a=new Array(t),i=new Date(e),o=0;t>o;)n=new Date(i),a[o++]=n,i.setDate(i.getDate()+1);return a},this._refreshView=function(){var t=this.activeDate.getFullYear(),a=this.activeDate.getMonth(),o=new Date(this.activeDate);o.setFullYear(t,a,1);var r=this.startingDay-o.getDay(),l=r>0?7-r:-r,s=new Date(o);l>0&&s.setDate(-l+1);for(var u=this.getDates(s,42),p=0;42>p;p++)u[p]=angular.extend(this.createDateObject(u[p],this.formatDay),{secondary:u[p].getMonth()!==a,uid:e.uniqueId+"-"+p});e.labels=new Array(7);for(var c=0;7>c;c++)e.labels[c]={abbr:n(u[c].date,this.formatDayHeader),full:n(u[c].date,"EEEE")};if(e.title=n(this.activeDate,this.formatDayTitle),e.rows=this.split(u,7),e.showWeeks){e.weekNumbers=[];for(var d=(11-this.startingDay)%7,m=e.rows.length,f=0;m>f;f++)e.weekNumbers.push(i(e.rows[f][d].date))}},this.compare=function(e,t){var n=new Date(e.getFullYear(),e.getMonth(),e.getDate()),a=new Date(t.getFullYear(),t.getMonth(),t.getDate());return n.setFullYear(e.getFullYear()),a.setFullYear(t.getFullYear()),n-a},this.handleKeyDown=function(e,t){var n=this.activeDate.getDate();if("left"===e)n-=1;else if("up"===e)n-=7;else if("right"===e)n+=1;else if("down"===e)n+=7;else if("pageup"===e||"pagedown"===e){var i=this.activeDate.getMonth()+("pageup"===e?-1:1);this.activeDate.setMonth(i,1),n=Math.min(a(this.activeDate.getFullYear(),this.activeDate.getMonth()),n)}else"home"===e?n=1:"end"===e&&(n=a(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(n)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(e,t,n){this.step={years:1},this.element=t,this.init=function(e){angular.extend(e,this),e.refreshView()},this._refreshView=function(){for(var t,a=new Array(12),i=this.activeDate.getFullYear(),o=0;12>o;o++)t=new Date(this.activeDate),t.setFullYear(i,o,1),a[o]=angular.extend(this.createDateObject(t,this.formatMonth),{uid:e.uniqueId+"-"+o});e.title=n(this.activeDate,this.formatMonthTitle),e.rows=this.split(a,this.monthColumns),e.yearHeaderColspan=this.monthColumns>3?this.monthColumns-2:1},this.compare=function(e,t){var n=new Date(e.getFullYear(),e.getMonth()),a=new Date(t.getFullYear(),t.getMonth());return n.setFullYear(e.getFullYear()),a.setFullYear(t.getFullYear()),n-a},this.handleKeyDown=function(e,t){var n=this.activeDate.getMonth();if("left"===e)n-=1;else if("up"===e)n-=this.monthColumns;else if("right"===e)n+=1;else if("down"===e)n+=this.monthColumns;else if("pageup"===e||"pagedown"===e){var a=this.activeDate.getFullYear()+("pageup"===e?-1:1);this.activeDate.setFullYear(a)}else"home"===e?n=0:"end"===e&&(n=11);this.activeDate.setMonth(n)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(e,t,n){function a(e){return parseInt((e-1)/o,10)*o+1}var i,o;this.element=t,this.yearpickerInit=function(){i=this.yearColumns,o=this.yearRows*i,this.step={years:o}},this._refreshView=function(){for(var t,n=new Array(o),r=0,l=a(this.activeDate.getFullYear());o>r;r++)t=new Date(this.activeDate),t.setFullYear(l+r,0,1),n[r]=angular.extend(this.createDateObject(t,this.formatYear),{uid:e.uniqueId+"-"+r});e.title=[n[0].label,n[o-1].label].join(" - "),e.rows=this.split(n,i),e.columns=i},this.compare=function(e,t){return e.getFullYear()-t.getFullYear()},this.handleKeyDown=function(e,t){var n=this.activeDate.getFullYear();"left"===e?n-=1:"up"===e?n-=i:"right"===e?n+=1:"down"===e?n+=i:"pageup"===e||"pagedown"===e?n+=("pageup"===e?-1:1)*o:"home"===e?n=a(this.activeDate.getFullYear()):"end"===e&&(n=a(this.activeDate.getFullYear())+o-1),this.activeDate.setFullYear(n)}}]).directive("uibDatepicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/datepicker.html"},scope:{datepickerOptions:"=?"},require:["uibDatepicker","^ngModel"],restrict:"A",controller:"UibDatepickerController",controllerAs:"datepicker",link:function(e,t,n,a){var i=a[0],o=a[1]; + i.init(o)}}}).directive("uibDaypicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/day.html"},require:["^uibDatepicker","uibDaypicker"],restrict:"A",controller:"UibDaypickerController",link:function(e,t,n,a){var i=a[0],o=a[1];o.init(i)}}}).directive("uibMonthpicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/month.html"},require:["^uibDatepicker","uibMonthpicker"],restrict:"A",controller:"UibMonthpickerController",link:function(e,t,n,a){var i=a[0],o=a[1];o.init(i)}}}).directive("uibYearpicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/year.html"},require:["^uibDatepicker","uibYearpicker"],restrict:"A",controller:"UibYearpickerController",link:function(e,t,n,a){var i=a[0];angular.extend(i,a[1]),i.yearpickerInit(),i.refreshView()}}}),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(e,t){var n,a,i={normal:/(auto|scroll)/,hidden:/(auto|scroll|hidden)/},o={auto:/\s?auto?\s?/i,primary:/^(top|bottom|left|right)$/,secondary:/^(top|bottom|left|right|center)$/,vertical:/^(top|bottom)$/},r=/(HTML|BODY)/;return{getRawNode:function(e){return e.nodeName?e:e[0]||e},parseStyle:function(e){return e=parseFloat(e),isFinite(e)?e:0},offsetParent:function(n){function a(e){return"static"===(t.getComputedStyle(e).position||"static")}n=this.getRawNode(n);for(var i=n.offsetParent||e[0].documentElement;i&&i!==e[0].documentElement&&a(i);)i=i.offsetParent;return i||e[0].documentElement},scrollbarWidth:function(i){if(i){if(angular.isUndefined(a)){var o=e.find("body");o.addClass("uib-position-body-scrollbar-measure"),a=t.innerWidth-o[0].clientWidth,a=isFinite(a)?a:0,o.removeClass("uib-position-body-scrollbar-measure")}return a}if(angular.isUndefined(n)){var r=angular.element('
        ');e.find("body").append(r),n=r[0].offsetWidth-r[0].clientWidth,n=isFinite(n)?n:0,r.remove()}return n},scrollbarPadding:function(e){e=this.getRawNode(e);var n=t.getComputedStyle(e),a=this.parseStyle(n.paddingRight),i=this.parseStyle(n.paddingBottom),o=this.scrollParent(e,!1,!0),l=this.scrollbarWidth(r.test(o.tagName));return{scrollbarWidth:l,widthOverflow:o.scrollWidth>o.clientWidth,right:a+l,originalRight:a,heightOverflow:o.scrollHeight>o.clientHeight,bottom:i+l,originalBottom:i}},isScrollable:function(e,n){e=this.getRawNode(e);var a=n?i.hidden:i.normal,o=t.getComputedStyle(e);return a.test(o.overflow+o.overflowY+o.overflowX)},scrollParent:function(n,a,o){n=this.getRawNode(n);var r=a?i.hidden:i.normal,l=e[0].documentElement,s=t.getComputedStyle(n);if(o&&r.test(s.overflow+s.overflowY+s.overflowX))return n;var u="absolute"===s.position,p=n.parentElement||l;if(p===l||"fixed"===s.position)return l;for(;p.parentElement&&p!==l;){var c=t.getComputedStyle(p);if(u&&"static"!==c.position&&(u=!1),!u&&r.test(c.overflow+c.overflowY+c.overflowX))break;p=p.parentElement}return p},position:function(n,a){n=this.getRawNode(n);var i=this.offset(n);if(a){var o=t.getComputedStyle(n);i.top-=this.parseStyle(o.marginTop),i.left-=this.parseStyle(o.marginLeft)}var r=this.offsetParent(n),l={top:0,left:0};return r!==e[0].documentElement&&(l=this.offset(r),l.top+=r.clientTop-r.scrollTop,l.left+=r.clientLeft-r.scrollLeft),{width:Math.round(angular.isNumber(i.width)?i.width:n.offsetWidth),height:Math.round(angular.isNumber(i.height)?i.height:n.offsetHeight),top:Math.round(i.top-l.top),left:Math.round(i.left-l.left)}},offset:function(n){n=this.getRawNode(n);var a=n.getBoundingClientRect();return{width:Math.round(angular.isNumber(a.width)?a.width:n.offsetWidth),height:Math.round(angular.isNumber(a.height)?a.height:n.offsetHeight),top:Math.round(a.top+(t.pageYOffset||e[0].documentElement.scrollTop)),left:Math.round(a.left+(t.pageXOffset||e[0].documentElement.scrollLeft))}},viewportOffset:function(n,a,i){n=this.getRawNode(n),i=i!==!1?!0:!1;var o=n.getBoundingClientRect(),r={top:0,left:0,bottom:0,right:0},l=a?e[0].documentElement:this.scrollParent(n),s=l.getBoundingClientRect();if(r.top=s.top+l.clientTop,r.left=s.left+l.clientLeft,l===e[0].documentElement&&(r.top+=t.pageYOffset,r.left+=t.pageXOffset),r.bottom=r.top+l.clientHeight,r.right=r.left+l.clientWidth,i){var u=t.getComputedStyle(l);r.top+=this.parseStyle(u.paddingTop),r.bottom-=this.parseStyle(u.paddingBottom),r.left+=this.parseStyle(u.paddingLeft),r.right-=this.parseStyle(u.paddingRight)}return{top:Math.round(o.top-r.top),bottom:Math.round(r.bottom-o.bottom),left:Math.round(o.left-r.left),right:Math.round(r.right-o.right)}},parsePlacement:function(e){var t=o.auto.test(e);return t&&(e=e.replace(o.auto,"")),e=e.split("-"),e[0]=e[0]||"top",o.primary.test(e[0])||(e[0]="top"),e[1]=e[1]||"center",o.secondary.test(e[1])||(e[1]="center"),t?e[2]=!0:e[2]=!1,e},positionElements:function(e,n,a,i){e=this.getRawNode(e),n=this.getRawNode(n);var r=angular.isDefined(n.offsetWidth)?n.offsetWidth:n.prop("offsetWidth"),l=angular.isDefined(n.offsetHeight)?n.offsetHeight:n.prop("offsetHeight");a=this.parsePlacement(a);var s=i?this.offset(e):this.position(e),u={top:0,left:0,placement:""};if(a[2]){var p=this.viewportOffset(e,i),c=t.getComputedStyle(n),d={width:r+Math.round(Math.abs(this.parseStyle(c.marginLeft)+this.parseStyle(c.marginRight))),height:l+Math.round(Math.abs(this.parseStyle(c.marginTop)+this.parseStyle(c.marginBottom)))};if(a[0]="top"===a[0]&&d.height>p.top&&d.height<=p.bottom?"bottom":"bottom"===a[0]&&d.height>p.bottom&&d.height<=p.top?"top":"left"===a[0]&&d.width>p.left&&d.width<=p.right?"right":"right"===a[0]&&d.width>p.right&&d.width<=p.left?"left":a[0],a[1]="top"===a[1]&&d.height-s.height>p.bottom&&d.height-s.height<=p.top?"bottom":"bottom"===a[1]&&d.height-s.height>p.top&&d.height-s.height<=p.bottom?"top":"left"===a[1]&&d.width-s.width>p.right&&d.width-s.width<=p.left?"right":"right"===a[1]&&d.width-s.width>p.left&&d.width-s.width<=p.right?"left":a[1],"center"===a[1])if(o.vertical.test(a[0])){var m=s.width/2-r/2;p.left+m<0&&d.width-s.width<=p.right?a[1]="left":p.right+m<0&&d.width-s.width<=p.left&&(a[1]="right")}else{var f=s.height/2-d.height/2;p.top+f<0&&d.height-s.height<=p.bottom?a[1]="top":p.bottom+f<0&&d.height-s.height<=p.top&&(a[1]="bottom")}}switch(a[0]){case"top":u.top=s.top-l;break;case"bottom":u.top=s.top+s.height;break;case"left":u.left=s.left-r;break;case"right":u.left=s.left+s.width}switch(a[1]){case"top":u.top=s.top;break;case"bottom":u.top=s.top+s.height-l;break;case"left":u.left=s.left;break;case"right":u.left=s.left+s.width-r;break;case"center":o.vertical.test(a[0])?u.left=s.left+s.width/2-r/2:u.top=s.top+s.height/2-l/2}return u.top=Math.round(u.top),u.left=Math.round(u.left),u.placement="center"===a[1]?a[0]:a[0]+"-"+a[1],u},adjustTop:function(e,t,n,a){return-1!==e.indexOf("top")&&n!==a?{top:t.top-a+"px"}:void 0},positionArrow:function(e,n){e=this.getRawNode(e);var a=e.querySelector(".tooltip-inner, .popover-inner");if(a){var i=angular.element(a).hasClass("tooltip-inner"),r=i?e.querySelector(".tooltip-arrow"):e.querySelector(".arrow");if(r){var l={top:"",bottom:"",left:"",right:""};if(n=this.parsePlacement(n),"center"===n[1])return void angular.element(r).css(l);var s="border-"+n[0]+"-width",u=t.getComputedStyle(r)[s],p="border-";p+=o.vertical.test(n[0])?n[0]+"-"+n[1]:n[1]+"-"+n[0],p+="-radius";var c=t.getComputedStyle(i?a:e)[p];switch(n[0]){case"top":l.bottom=i?"0":"-"+u;break;case"bottom":l.top=i?"0":"-"+u;break;case"left":l.right=i?"0":"-"+u;break;case"right":l.left=i?"0":"-"+u}l[n[1]]=c,angular.element(r).css(l)}}}}}]),angular.module("ui.bootstrap.datepickerPopup",["ui.bootstrap.datepicker","ui.bootstrap.position"]).value("$datepickerPopupLiteralWarning",!0).constant("uibDatepickerPopupConfig",{altInputFormats:[],appendToBody:!1,clearText:"Clear",closeOnDateSelection:!0,closeText:"Done",currentText:"Today",datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"uib/template/datepickerPopup/popup.html",datepickerTemplateUrl:"uib/template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},onOpenFocus:!0,showButtonBar:!0,placement:"auto bottom-left"}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$log","$parse","$window","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout","uibDatepickerConfig","$datepickerPopupLiteralWarning",function(e,t,n,a,i,o,r,l,s,u,p,c,d,m,f,h){function g(t){var n=c.parse(t,D,e.date);if(isNaN(n))for(var a=0;a
        "),S.attr({"ng-model":"date","ng-change":"dateSelection(date)","template-url":T}),P=angular.element(S.children()[0]),P.attr("template-url",O),e.datepickerOptions||(e.datepickerOptions={}),F&&"month"===n.type&&(e.datepickerOptions.datepickerMode="month",e.datepickerOptions.minMode="month"),P.attr("datepicker-options","datepickerOptions"),F?A.$formatters.push(function(t){return e.date=c.fromTimezone(t,I.getOption("timezone")),t}):(A.$$parserName="date",A.$validators.date=v,A.$parsers.unshift(b),A.$formatters.push(function(t){return A.$isEmpty(t)?(e.date=t,t):(angular.isNumber(t)&&(t=new Date(t)),e.date=c.fromTimezone(t,I.getOption("timezone")),c.filter(e.date,D))})),A.$viewChangeListeners.push(function(){e.date=g(A.$viewValue)}),t.on("keydown",y),U=a(S)(e),S.remove(),C?l.find("body").append(U):t.after(U),e.$on("$destroy",function(){for(e.isOpen===!0&&(s.$$phase||e.$apply(function(){e.isOpen=!1})),U.remove(),t.off("keydown",y),l.off("click",$),E&&E.off("scroll",w),angular.element(r).off("resize",w);H.length;)H.shift()()})},e.getText=function(t){return e[t+"Text"]||d[t+"Text"]},e.isDisabled=function(t){"today"===t&&(t=c.fromTimezone(new Date,I.getOption("timezone")));var n={};return angular.forEach(["minDate","maxDate"],function(t){e.datepickerOptions[t]?angular.isDate(e.datepickerOptions[t])?n[t]=new Date(e.datepickerOptions[t]):(h&&i.warn("Literal date support has been deprecated, please switch to date object usage"),n[t]=new Date(p(e.datepickerOptions[t],"medium"))):n[t]=null}),e.datepickerOptions&&n.minDate&&e.compare(t,n.minDate)<0||n.maxDate&&e.compare(t,n.maxDate)>0},e.compare=function(e,t){return new Date(e.getFullYear(),e.getMonth(),e.getDate())-new Date(t.getFullYear(),t.getMonth(),t.getDate())},e.dateSelection=function(n){e.date=n;var a=e.date?c.filter(e.date,D):null;t.val(a),A.$setViewValue(a),x&&(e.isOpen=!1,t[0].focus())},e.keydown=function(n){27===n.which&&(n.stopPropagation(),e.isOpen=!1,t[0].focus())},e.select=function(t,n){if(n.stopPropagation(),"today"===t){var a=new Date;angular.isDate(e.date)?(t=new Date(e.date),t.setFullYear(a.getFullYear(),a.getMonth(),a.getDate())):(t=c.fromTimezone(a,I.getOption("timezone")),t.setHours(0,0,0,0))}e.dateSelection(t)},e.close=function(n){n.stopPropagation(),e.isOpen=!1,t[0].focus()},e.disabled=angular.isDefined(n.disabled)||!1,n.ngDisabled&&H.push(e.$parent.$watch(o(n.ngDisabled),function(t){e.disabled=t})),e.$watch("isOpen",function(a){a?e.disabled?e.isOpen=!1:m(function(){w(),M&&e.$broadcast("uib:datepicker.focus"),l.on("click",$);var a=n.popupPlacement?n.popupPlacement:d.placement;C||u.parsePlacement(a)[2]?(E=E||angular.element(u.scrollParent(t)),E&&E.on("scroll",w)):E=null,angular.element(r).on("resize",w)},0,!1):(l.off("click",$),E&&E.off("scroll",w),angular.element(r).off("resize",w))}),e.$on("uib:datepicker.mode",function(){m(w,0,!1)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{datepickerOptions:"=?",isOpen:"=?",currentText:"@",clearText:"@",closeText:"@"},link:function(e,t,n,a){var i=a[0],o=a[1];o.init(i)}}}).directive("uibDatepickerPopupWrap",function(){return{restrict:"A",transclude:!0,templateUrl:function(e,t){return t.templateUrl||"uib/template/datepickerPopup/popup.html"}}}),angular.module("ui.bootstrap.debounce",[]).factory("$$debounce",["$timeout",function(e){return function(t,n){var a;return function(){var i=this,o=Array.prototype.slice.call(arguments);a&&e.cancel(a),a=e(function(){t.apply(i,o)},n)}}}]),angular.module("ui.bootstrap.multiMap",[]).factory("$$multiMap",function(){return{createNew:function(){var e={};return{entries:function(){return Object.keys(e).map(function(t){return{key:t,value:e[t]}})},get:function(t){return e[t]},hasKey:function(t){return!!e[t]},keys:function(){return Object.keys(e)},put:function(t,n){e[t]||(e[t]=[]),e[t].push(n)},remove:function(t,n){var a=e[t];if(a){var i=a.indexOf(n);-1!==i&&a.splice(i,1),a.length||delete e[t]}}}}}}),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.multiMap","ui.bootstrap.position"]).constant("uibDropdownConfig",{appendToOpenClass:"uib-dropdown-open",openClass:"open"}).service("uibDropdownService",["$document","$rootScope","$$multiMap",function(e,t,n){var a=null,i=n.createNew();this.isOnlyOpen=function(e,t){var n=i.get(t);if(n){var a=n.reduce(function(t,n){return n.scope===e?n:t},{});if(a)return 1===n.length}return!1},this.open=function(t,n,r){if(a||e.on("click",o),a&&a!==t&&(a.isOpen=!1),a=t,r){var l=i.get(r);if(l){var s=l.map(function(e){return e.scope});-1===s.indexOf(t)&&i.put(r,{scope:t})}else i.put(r,{scope:t})}},this.close=function(t,n,r){if(a===t&&(e.off("click",o),e.off("keydown",this.keybindFilter),a=null),r){var l=i.get(r);if(l){var s=l.reduce(function(e,n){return n.scope===t?n:e},{});s&&i.remove(r,s)}}};var o=function(e){if(a&&a.isOpen&&!(e&&"disabled"===a.getAutoClose()||e&&3===e.which)){var n=a.getToggleElement();if(!(e&&n&&n[0].contains(e.target))){var i=a.getDropdownElement();e&&"outsideClick"===a.getAutoClose()&&i&&i[0].contains(e.target)||(a.focusToggleElement(),a.isOpen=!1,t.$$phase||a.$apply())}}};this.keybindFilter=function(e){if(a){var t=a.getDropdownElement(),n=a.getToggleElement(),i=t&&t[0].contains(e.target),r=n&&n[0].contains(e.target);27===e.which?(e.stopPropagation(),a.focusToggleElement(),o()):a.isKeynavEnabled()&&-1!==[38,40].indexOf(e.which)&&a.isOpen&&(i||r)&&(e.preventDefault(),e.stopPropagation(),a.focusDropdownEntry(e.which))}}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(e,t,n,a,i,o,r,l,s,u,p){function c(){t.append(f.dropdownMenu)}var d,m,f=this,h=e.$new(),g=i.appendToOpenClass,b=i.openClass,v=angular.noop,$=n.onToggle?a(n.onToggle):angular.noop,y=!1,w=s.find("body");t.addClass("dropdown"),this.init=function(){n.isOpen&&(m=a(n.isOpen),v=m.assign,e.$watch(m,function(e){h.isOpen=!!e})),y=angular.isDefined(n.keyboardNav)},this.toggle=function(e){return h.isOpen=arguments.length?!!e:!h.isOpen,angular.isFunction(v)&&v(h,h.isOpen),h.isOpen},this.isOpen=function(){return h.isOpen},h.getToggleElement=function(){return f.toggleElement},h.getAutoClose=function(){return n.autoClose||"always"},h.getElement=function(){return t},h.isKeynavEnabled=function(){return y},h.focusDropdownEntry=function(e){var n=f.dropdownMenu?angular.element(f.dropdownMenu).find("a"):t.find("ul").eq(0).find("a");switch(e){case 40:angular.isNumber(f.selectedOption)?f.selectedOption=f.selectedOption===n.length-1?f.selectedOption:f.selectedOption+1:f.selectedOption=0;break;case 38:angular.isNumber(f.selectedOption)?f.selectedOption=0===f.selectedOption?0:f.selectedOption-1:f.selectedOption=n.length-1}n[f.selectedOption].focus()},h.getDropdownElement=function(){return f.dropdownMenu},h.focusToggleElement=function(){f.toggleElement&&f.toggleElement[0].focus()},h.$watch("isOpen",function(i,m){var y=null,k=!1;if(angular.isDefined(n.dropdownAppendTo)){var D=a(n.dropdownAppendTo)(h);D&&(y=angular.element(D))}if(angular.isDefined(n.dropdownAppendToBody)){var x=a(n.dropdownAppendToBody)(h);x!==!1&&(k=!0)}if(k&&!y&&(y=w),y&&f.dropdownMenu&&(i?(y.append(f.dropdownMenu),t.on("$destroy",c)):(t.off("$destroy",c),c())),y&&f.dropdownMenu){var C,M,T,O=l.positionElements(t,f.dropdownMenu,"bottom-left",!0),S=0;if(C={top:O.top+"px",display:i?"block":"none"},M=f.dropdownMenu.hasClass("dropdown-menu-right"),M?(C.left="auto",T=l.scrollbarPadding(y),T.heightOverflow&&T.scrollbarWidth&&(S=T.scrollbarWidth),C.right=window.innerWidth-S-(O.left+t.prop("offsetWidth"))+"px"):(C.left=O.left+"px",C.right="auto"),!k){var P=l.offset(y);C.top=O.top-P.top+"px",M?C.right=window.innerWidth-(O.left-P.left+t.prop("offsetWidth"))+"px":C.left=O.left-P.left+"px"}f.dropdownMenu.css(C)}var E=y?y:t,A=y?g:b,I=E.hasClass(A),U=o.isOnlyOpen(e,y);if(I===!i){var N;N=y?U?"removeClass":"addClass":i?"addClass":"removeClass",r[N](E,A).then(function(){angular.isDefined(i)&&i!==m&&$(e,{open:!!i})})}if(i)f.dropdownMenuTemplateUrl?p(f.dropdownMenuTemplateUrl).then(function(e){d=h.$new(),u(e.trim())(d,function(e){var t=e;f.dropdownMenu.replaceWith(t),f.dropdownMenu=t,s.on("keydown",o.keybindFilter)})}):s.on("keydown",o.keybindFilter),h.focusToggleElement(),o.open(h,t,y);else{if(o.close(h,t,y),f.dropdownMenuTemplateUrl){d&&d.$destroy();var F=angular.element('');f.dropdownMenu.replaceWith(F),f.dropdownMenu=F}f.selectedOption=null}angular.isFunction(v)&&v(e,i)})}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(e,t,n,a){a.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"A",require:"?^uibDropdown",link:function(e,t,n,a){if(a&&!angular.isDefined(n.dropdownNested)){t.addClass("dropdown-menu");var i=n.templateUrl;i&&(a.dropdownMenuTemplateUrl=i),a.dropdownMenu||(a.dropdownMenu=t)}}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(e,t,n,a){if(a){t.addClass("dropdown-toggle"),a.toggleElement=t;var i=function(i){i.preventDefault(),t.hasClass("disabled")||n.disabled||e.$apply(function(){a.toggle()})};t.on("click",i),t.attr({"aria-haspopup":!0,"aria-expanded":!1}),e.$watch(a.isOpen,function(e){t.attr("aria-expanded",!!e)}),e.$on("$destroy",function(){t.off("click",i)})}}}}),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var e=[];return{add:function(t,n){e.push({key:t,value:n})},get:function(t){for(var n=0;n-1&&M>e&&(e=M),e}function d(e,t){var n=D.get(e).value,a=n.appendTo;D.remove(e),T=D.top(),T&&(M=parseInt(T.value.modalDomEl.attr("index"),10)),h(n.modalDomEl,n.modalScope,function(){var t=n.openedClass||k;x.remove(t,e);var i=x.hasKey(t);a.toggleClass(t,i),!i&&w&&w.heightOverflow&&w.scrollbarWidth&&(w.originalRight?a.css({paddingRight:w.originalRight+"px"}):a.css({paddingRight:""}),w=null),m(!0)},n.closedDeferred),f(),t&&t.focus?t.focus():a.focus&&a.focus()}function m(e){var t;D.length()>0&&(t=D.top().value,t.modalDomEl.toggleClass(t.windowTopClass||"",e))}function f(){if($&&-1===c()){var e=y;h($,y,function(){e=null}),$=void 0,y=void 0}}function h(t,n,a,i){function r(){r.done||(r.done=!0,e.leave(t).then(function(){a&&a(),t.remove(),i&&i.resolve()}),n.$destroy())}var l,s=null,u=function(){return l||(l=o.defer(),s=l.promise),function(){l.resolve()}};return n.$broadcast(C.NOW_CLOSING_EVENT,u),o.when(s).then(r)}function g(e){if(e.isDefaultPrevented())return e;var t=D.top();if(t)switch(e.which){case 27:t.value.keyboard&&(e.preventDefault(),i.$apply(function(){C.dismiss(t.key,"escape key press")}));break;case 9:var n=C.loadFocusElementList(t),a=!1;e.shiftKey?(C.isFocusInFirstItem(e,n)||C.isModalFocused(e,t))&&(a=C.focusLastFocusableElement(n)):C.isFocusInLastItem(e,n)&&(a=C.focusFirstFocusableElement(n)),a&&(e.preventDefault(),e.stopPropagation())}}function b(e,t,n){return!e.value.modalScope.$broadcast("modal.closing",t,n).defaultPrevented}function v(){Array.prototype.forEach.call(document.querySelectorAll("["+O+"]"),function(e){var t=parseInt(e.getAttribute(O),10),n=t-1;e.setAttribute(O,n),n||(e.removeAttribute(O),e.removeAttribute("aria-hidden"))})}var $,y,w,k="modal-open",D=l.createNew(),x=r.createNew(),C={NOW_CLOSING_EVENT:"modal.stack.now-closing"},M=0,T=null,O="data-bootstrap-modal-aria-hidden-count",S="a[href], area[href], input:not([disabled]):not([tabindex='-1']), button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']), textarea:not([disabled]):not([tabindex='-1']), iframe, object, embed, *[tabindex]:not([tabindex='-1']), *[contenteditable=true]",P=/[A-Z]/g;return i.$watch(c,function(e){y&&(y.index=e)}),n.on("keydown",g),i.$on("$destroy",function(){n.off("keydown",g)}),C.open=function(t,o){function r(e){function t(e){var t=e.parent()?e.parent().children():[];return Array.prototype.filter.call(t,function(t){return t!==e[0]})}if(e&&"BODY"!==e[0].tagName)return t(e).forEach(function(e){var t="true"===e.getAttribute("aria-hidden"),n=parseInt(e.getAttribute(O),10);n||(n=t?1:0),e.setAttribute(O,n+1),e.setAttribute("aria-hidden","true")}),r(e.parent())}var l=n[0].activeElement,p=o.openedClass||k;m(!1),T=D.top(),D.add(t,{deferred:o.deferred,renderDeferred:o.renderDeferred,closedDeferred:o.closedDeferred,modalScope:o.scope,backdrop:o.backdrop,keyboard:o.keyboard,openedClass:o.openedClass,windowTopClass:o.windowTopClass,animation:o.animation,appendTo:o.appendTo}),x.put(p,t);var d=o.appendTo,f=c();f>=0&&!$&&(y=i.$new(!0),y.modalOptions=o,y.index=f,$=angular.element('
        '),$.attr({"class":"modal-backdrop","ng-style":"{'z-index': 1040 + (index && 1 || 0) + index*10}","uib-modal-animation-class":"fade","modal-in-class":"in"}),o.backdropClass&&$.addClass(o.backdropClass),o.animation&&$.attr("modal-animation","true"),a($)(y),e.enter($,d),s.isScrollable(d)&&(w=s.scrollbarPadding(d),w.heightOverflow&&w.scrollbarWidth&&d.css({paddingRight:w.right+"px"})));var h;o.component?(h=document.createElement(u(o.component.name)),h=angular.element(h),h.attr({resolve:"$resolve","modal-instance":"$uibModalInstance",close:"$close($value)",dismiss:"$dismiss($value)"})):h=o.content,M=T?parseInt(T.value.modalDomEl.attr("index"),10)+1:0;var g=angular.element('
        ');g.attr({"class":"modal","template-url":o.windowTemplateUrl,"window-top-class":o.windowTopClass,role:"dialog","aria-labelledby":o.ariaLabelledBy,"aria-describedby":o.ariaDescribedBy,size:o.size,index:M,animate:"animate","ng-style":"{'z-index': 1050 + $$topModalIndex*10, display: 'block'}",tabindex:-1,"uib-modal-animation-class":"fade","modal-in-class":"in"}).append(h),o.windowClass&&g.addClass(o.windowClass),o.animation&&g.attr("modal-animation","true"),d.addClass(p),o.scope&&(o.scope.$$topModalIndex=M),e.enter(a(g)(o.scope),d),D.top().value.modalDomEl=g,D.top().value.modalOpener=l,r(g)},C.close=function(e,t){var n=D.get(e);return v(),n&&b(n,t,!0)?(n.value.modalScope.$$uibDestructionScheduled=!0,n.value.deferred.resolve(t),d(e,n.value.modalOpener),!0):!n},C.dismiss=function(e,t){var n=D.get(e);return v(),n&&b(n,t,!1)?(n.value.modalScope.$$uibDestructionScheduled=!0,n.value.deferred.reject(t),d(e,n.value.modalOpener),!0):!n},C.dismissAll=function(e){for(var t=this.getTop();t&&this.dismiss(t.key,e);)t=this.getTop()},C.getTop=function(){return D.top()},C.modalRendered=function(e){var t=D.get(e);t&&t.value.renderDeferred.resolve()},C.focusFirstFocusableElement=function(e){return e.length>0?(e[0].focus(),!0):!1},C.focusLastFocusableElement=function(e){return e.length>0?(e[e.length-1].focus(),!0):!1},C.isModalFocused=function(e,t){if(e&&t){var n=t.value.modalDomEl;if(n&&n.length)return(e.target||e.srcElement)===n[0]}return!1},C.isFocusInFirstItem=function(e,t){return t.length>0?(e.target||e.srcElement)===t[0]:!1},C.isFocusInLastItem=function(e,t){return t.length>0?(e.target||e.srcElement)===t[t.length-1]:!1},C.loadFocusElementList=function(e){if(e){var t=e.value.modalDomEl;if(t&&t.length){var n=t[0].querySelectorAll(S);return n?Array.prototype.filter.call(n,function(e){return p(e)}):n}}},C}]).provider("$uibModal",function(){var e={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$rootScope","$q","$document","$templateRequest","$controller","$uibResolve","$uibModalStack",function(t,n,a,i,o,r,l){function s(e){return e.template?n.when(e.template):i(angular.isFunction(e.templateUrl)?e.templateUrl():e.templateUrl)}var u={},p=null;return u.getPromiseChain=function(){return p},u.open=function(i){function u(){return g}var c=n.defer(),d=n.defer(),m=n.defer(),f=n.defer(),h={result:c.promise,opened:d.promise,closed:m.promise,rendered:f.promise,close:function(e){return l.close(h,e)},dismiss:function(e){return l.dismiss(h,e)}};if(i=angular.extend({},e.options,i),i.resolve=i.resolve||{},i.appendTo=i.appendTo||a.find("body").eq(0),!i.appendTo.length)throw new Error("appendTo element not found. Make sure that the element passed is in DOM.");if(!i.component&&!i.template&&!i.templateUrl)throw new Error("One of component or template or templateUrl options is required.");var g;g=i.component?n.when(r.resolve(i.resolve,{},null,null)):n.all([s(i),r.resolve(i.resolve,{},null,null)]);var b;return b=p=n.all([p]).then(u,u).then(function(e){function n(t,n,a,i){t.$scope=r,t.$scope.$resolve={},a?t.$scope.$uibModalInstance=h:t.$uibModalInstance=h;var o=n?e[1]:e;angular.forEach(o,function(e,n){i&&(t[n]=e),t.$scope.$resolve[n]=e})}var a=i.scope||t,r=a.$new();r.$close=h.close,r.$dismiss=h.dismiss,r.$on("$destroy",function(){r.$$uibDestructionScheduled||r.$dismiss("$uibUnscheduledDestruction")});var s,u,p={scope:r,deferred:c,renderDeferred:f,closedDeferred:m,animation:i.animation,backdrop:i.backdrop,keyboard:i.keyboard,backdropClass:i.backdropClass,windowTopClass:i.windowTopClass,windowClass:i.windowClass,windowTemplateUrl:i.windowTemplateUrl,ariaLabelledBy:i.ariaLabelledBy,ariaDescribedBy:i.ariaDescribedBy,size:i.size,openedClass:i.openedClass,appendTo:i.appendTo},g={},b={};i.component?(n(g,!1,!0,!1),g.name=i.component,p.component=g):i.controller&&(n(b,!0,!1,!0),u=o(i.controller,b,!0,i.controllerAs),i.controllerAs&&i.bindToController&&(s=u.instance,s.$close=r.$close,s.$dismiss=r.$dismiss,angular.extend(s,{$resolve:b.$scope.$resolve},a)),s=u(),angular.isFunction(s.$onInit)&&s.$onInit()),i.component||(p.content=e[0]),l.open(h,p),d.resolve(!0)},function(e){d.reject(e),c.reject(e)})["finally"](function(){p===b&&(p=null)}),h},u}]};return e}),angular.module("ui.bootstrap.paging",[]).factory("uibPaging",["$parse",function(e){return{create:function(t,n,a){t.setNumPages=a.numPages?e(a.numPages).assign:angular.noop,t.ngModelCtrl={$setViewValue:angular.noop + },t._watchers=[],t.init=function(e,i){t.ngModelCtrl=e,t.config=i,e.$render=function(){t.render()},a.itemsPerPage?t._watchers.push(n.$parent.$watch(a.itemsPerPage,function(e){t.itemsPerPage=parseInt(e,10),n.totalPages=t.calculateTotalPages(),t.updatePage()})):t.itemsPerPage=i.itemsPerPage,n.$watch("totalItems",function(e,a){(angular.isDefined(e)||e!==a)&&(n.totalPages=t.calculateTotalPages(),t.updatePage())})},t.calculateTotalPages=function(){var e=t.itemsPerPage<1?1:Math.ceil(n.totalItems/t.itemsPerPage);return Math.max(e||0,1)},t.render=function(){n.page=parseInt(t.ngModelCtrl.$viewValue,10)||1},n.selectPage=function(e,a){a&&a.preventDefault();var i=!n.ngDisabled||!a;i&&n.page!==e&&e>0&&e<=n.totalPages&&(a&&a.target&&a.target.blur(),t.ngModelCtrl.$setViewValue(e),t.ngModelCtrl.$render())},n.getText=function(e){return n[e+"Text"]||t.config[e+"Text"]},n.noPrevious=function(){return 1===n.page},n.noNext=function(){return n.page===n.totalPages},t.updatePage=function(){t.setNumPages(n.$parent,n.totalPages),n.page>n.totalPages?n.selectPage(n.totalPages):t.ngModelCtrl.$render()},n.$on("$destroy",function(){for(;t._watchers.length;)t._watchers.shift()()})}}}]),angular.module("ui.bootstrap.pager",["ui.bootstrap.paging","ui.bootstrap.tabindex"]).controller("UibPagerController",["$scope","$attrs","uibPaging","uibPagerConfig",function(e,t,n,a){e.align=angular.isDefined(t.align)?e.$parent.$eval(t.align):a.align,n.create(this,e,t)}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(e){return{scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],restrict:"A",controller:"UibPagerController",controllerAs:"pager",templateUrl:function(e,t){return t.templateUrl||"uib/template/pager/pager.html"},link:function(t,n,a,i){n.addClass("pager");var o=i[0],r=i[1];r&&o.init(r,e)}}}]),angular.module("ui.bootstrap.pagination",["ui.bootstrap.paging","ui.bootstrap.tabindex"]).controller("UibPaginationController",["$scope","$attrs","$parse","uibPaging","uibPaginationConfig",function(e,t,n,a,i){function o(e,t,n){return{number:e,text:t,active:n}}function r(e,t){var n=[],a=1,i=t,r=angular.isDefined(s)&&t>s;r&&(u?(a=Math.max(e-Math.floor(s/2),1),i=a+s-1,i>t&&(i=t,a=i-s+1)):(a=(Math.ceil(e/s)-1)*s+1,i=Math.min(a+s-1,t)));for(var l=a;i>=l;l++){var m=o(l,d(l),l===e);n.push(m)}if(r&&s>0&&(!u||p||c)){if(a>1){if(!c||a>3){var f=o(a-1,"...",!1);n.unshift(f)}if(c){if(3===a){var h=o(2,"2",!1);n.unshift(h)}var g=o(1,"1",!1);n.unshift(g)}}if(t>i){if(!c||t-2>i){var b=o(i+1,"...",!1);n.push(b)}if(c){if(i===t-2){var v=o(t-1,t-1,!1);n.push(v)}var $=o(t,t,!1);n.push($)}}}return n}var l=this,s=angular.isDefined(t.maxSize)?e.$parent.$eval(t.maxSize):i.maxSize,u=angular.isDefined(t.rotate)?e.$parent.$eval(t.rotate):i.rotate,p=angular.isDefined(t.forceEllipses)?e.$parent.$eval(t.forceEllipses):i.forceEllipses,c=angular.isDefined(t.boundaryLinkNumbers)?e.$parent.$eval(t.boundaryLinkNumbers):i.boundaryLinkNumbers,d=angular.isDefined(t.pageLabel)?function(n){return e.$parent.$eval(t.pageLabel,{$page:n})}:angular.identity;e.boundaryLinks=angular.isDefined(t.boundaryLinks)?e.$parent.$eval(t.boundaryLinks):i.boundaryLinks,e.directionLinks=angular.isDefined(t.directionLinks)?e.$parent.$eval(t.directionLinks):i.directionLinks,t.$set("role","menu"),a.create(this,e,t),t.maxSize&&l._watchers.push(e.$parent.$watch(n(t.maxSize),function(e){s=parseInt(e,10),l.render()}));var m=this.render;this.render=function(){m(),e.page>0&&e.page<=e.totalPages&&(e.pages=r(e.page,e.totalPages))}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,boundaryLinkNumbers:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0,forceEllipses:!1}).directive("uibPagination",["$parse","uibPaginationConfig",function(e,t){return{scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],restrict:"A",controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(e,t){return t.templateUrl||"uib/template/pagination/pagination.html"},link:function(e,n,a,i){n.addClass("pagination");var o=i[0],r=i[1];r&&o.init(r,t)}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function e(e){var t=/[A-Z]/g,n="-";return e.replace(t,function(e,t){return(t?n:"")+e.toLowerCase()})}var t={placement:"top",placementClassPrefix:"",animation:!0,popupDelay:0,popupCloseDelay:0,useContentExp:!1},n={mouseenter:"mouseleave",click:"click",outsideClick:"outsideClick",focus:"blur",none:""},a={};this.options=function(e){angular.extend(a,e)},this.setTriggers=function(e){angular.extend(n,e)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(i,o,r,l,s,u,p,c,d){function m(e){if(27===e.which){var t=f.top();t&&(t.value.close(),t=null)}}var f=d.createNew();return l.on("keyup",m),p.$on("$destroy",function(){l.off("keyup",m)}),function(i,p,d,m){function h(e){var t=(e||m.trigger||d).split(" "),a=t.map(function(e){return n[e]||e});return{show:t,hide:a}}m=angular.extend({},t,a,m);var g=e(i),b=u.startSymbol(),v=u.endSymbol(),$="
        ';return{compile:function(e,t){var n=o($);return function(e,t,a,o){function u(){L.isOpen?g():d()}function d(){(!q||e.$eval(a[p+"Enable"]))&&(y(),D(),L.popupDelay?I||(I=r(b,L.popupDelay,!1)):b())}function g(){v(),L.popupCloseDelay?U||(U=r($,L.popupCloseDelay,!1)):$()}function b(){return v(),y(),L.content?(w(),void L.$evalAsync(function(){L.isOpen=!0,x(!0),j()})):angular.noop}function v(){I&&(r.cancel(I),I=null),N&&(r.cancel(N),N=null)}function $(){L&&L.$evalAsync(function(){L&&(L.isOpen=!1,x(!1),L.animation?A||(A=r(k,150,!1)):k())})}function y(){U&&(r.cancel(U),U=null),A&&(r.cancel(A),A=null)}function w(){P||(E=L.$new(),P=n(E,function(e){R?l.find("body").append(e):t.after(e)}),f.add(L,{close:$}),C())}function k(){v(),y(),M(),P&&(P.remove(),P=null,F&&r.cancel(F)),f.remove(L),E&&(E.$destroy(),E=null)}function D(){L.title=a[p+"Title"],B?L.content=B(e):L.content=a[i],L.popupClass=a[p+"Class"],L.placement=angular.isDefined(a[p+"Placement"])?a[p+"Placement"]:m.placement;var t=s.parsePlacement(L.placement);H=t[1]?t[0]+"-"+t[1]:t[0];var n=parseInt(a[p+"PopupDelay"],10),o=parseInt(a[p+"PopupCloseDelay"],10);L.popupDelay=isNaN(n)?m.popupDelay:n,L.popupCloseDelay=isNaN(o)?m.popupCloseDelay:o}function x(t){Y&&angular.isFunction(Y.assign)&&Y.assign(e,t)}function C(){W.length=0,B?(W.push(e.$watch(B,function(e){L.content=e,!e&&L.isOpen&&$()})),W.push(E.$watch(function(){z||(z=!0,E.$$postDigest(function(){z=!1,L&&L.isOpen&&j()}))}))):W.push(a.$observe(i,function(e){L.content=e,!e&&L.isOpen?$():j()})),W.push(a.$observe(p+"Title",function(e){L.title=e,L.isOpen&&j()})),W.push(a.$observe(p+"Placement",function(e){L.placement=e?e:m.placement,L.isOpen&&j()}))}function M(){W.length&&(angular.forEach(W,function(e){e()}),W.length=0)}function T(e){L&&L.isOpen&&P&&(t[0].contains(e.target)||P[0].contains(e.target)||g())}function O(e){27===e.which&&g()}function S(){var n=[],i=[],o=e.$eval(a[p+"Trigger"]);_(),angular.isObject(o)?(Object.keys(o).forEach(function(e){n.push(e),i.push(o[e])}),V={show:n,hide:i}):V=h(o),"none"!==V.show&&V.show.forEach(function(e,n){"outsideClick"===e?(t.on("click",u),l.on("click",T)):e===V.hide[n]?t.on(e,u):e&&(t.on(e,d),t.on(V.hide[n],g)),t.on("keypress",O)})}var P,E,A,I,U,N,F,H,R=angular.isDefined(m.appendToBody)?m.appendToBody:!1,V=h(void 0),q=angular.isDefined(a[p+"Enable"]),L=e.$new(!0),z=!1,Y=angular.isDefined(a[p+"IsOpen"])?c(a[p+"IsOpen"]):!1,B=m.useContentExp?c(a[i]):!1,W=[],j=function(){P&&P.html()&&(N||(N=r(function(){var e=s.positionElements(t,P,L.placement,R),n=angular.isDefined(P.offsetHeight)?P.offsetHeight:P.prop("offsetHeight"),a=R?s.offset(t):s.position(t);P.css({top:e.top+"px",left:e.left+"px"});var i=e.placement.split("-");P.hasClass(i[0])||(P.removeClass(H.split("-")[0]),P.addClass(i[0])),P.hasClass(m.placementClassPrefix+e.placement)||(P.removeClass(m.placementClassPrefix+H),P.addClass(m.placementClassPrefix+e.placement)),F=r(function(){var e=angular.isDefined(P.offsetHeight)?P.offsetHeight:P.prop("offsetHeight"),t=s.adjustTop(i,a,n,e);t&&P.css(t),F=null},0,!1),P.hasClass("uib-position-measure")?(s.positionArrow(P,e.placement),P.removeClass("uib-position-measure")):H!==e.placement&&s.positionArrow(P,e.placement),H=e.placement,N=null},0,!1)))};L.origScope=e,L.isOpen=!1,L.contentExp=function(){return L.content},a.$observe("disabled",function(e){e&&v(),e&&L.isOpen&&$()}),Y&&e.$watch(Y,function(e){L&&!e===L.isOpen&&u()});var _=function(){V.show.forEach(function(e){"outsideClick"===e?t.off("click",u):(t.off(e,d),t.off(e,u)),t.off("keypress",O)}),V.hide.forEach(function(e){"outsideClick"===e?l.off("click",T):t.off(e,g)})};S();var G=e.$eval(a[p+"Animation"]);L.animation=angular.isDefined(G)?!!G:m.animation;var K,X=p+"AppendToBody";K=X in a&&void 0===a[X]?!0:e.$eval(a[X]),R=angular.isDefined(K)?K:R,e.$on("$destroy",function(){_(),k(),L=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(e,t,n,a){return{link:function(i,o,r){var l,s,u,p=i.$eval(r.tooltipTemplateTranscludeScope),c=0,d=function(){s&&(s.remove(),s=null),l&&(l.$destroy(),l=null),u&&(e.leave(u).then(function(){s=null}),s=u,u=null)};i.$watch(t.parseAsResourceUrl(r.uibTooltipTemplateTransclude),function(t){var r=++c;t?(a(t,!0).then(function(a){if(r===c){var i=p.$new(),s=a,m=n(s)(i,function(t){d(),e.enter(t,o)});l=i,u=m,l.$emit("$includeContentLoaded",t)}},function(){r===c&&(d(),i.$emit("$includeContentError",t))}),i.$emit("$includeContentRequested",t)):d()}),i.$on("$destroy",d)}}}]).directive("uibTooltipClasses",["$uibPosition",function(e){return{restrict:"A",link:function(t,n,a){if(t.placement){var i=e.parsePlacement(t.placement);n.addClass(i[0])}t.popupClass&&n.addClass(t.popupClass),t.animation&&n.addClass(a.tooltipAnimationClass)}}}]).directive("uibTooltipPopup",function(){return{restrict:"A",scope:{content:"@"},templateUrl:"uib/template/tooltip/tooltip-popup.html"}}).directive("uibTooltip",["$uibTooltip",function(e){return e("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{restrict:"A",scope:{contentExp:"&",originScope:"&"},templateUrl:"uib/template/tooltip/tooltip-template-popup.html"}}).directive("uibTooltipTemplate",["$uibTooltip",function(e){return e("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{restrict:"A",scope:{contentExp:"&"},templateUrl:"uib/template/tooltip/tooltip-html-popup.html"}}).directive("uibTooltipHtml",["$uibTooltip",function(e){return e("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{restrict:"A",scope:{uibTitle:"@",contentExp:"&",originScope:"&"},templateUrl:"uib/template/popover/popover-template.html"}}).directive("uibPopoverTemplate",["$uibTooltip",function(e){return e("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{restrict:"A",scope:{contentExp:"&",uibTitle:"@"},templateUrl:"uib/template/popover/popover-html.html"}}).directive("uibPopoverHtml",["$uibTooltip",function(e){return e("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{restrict:"A",scope:{uibTitle:"@",content:"@"},templateUrl:"uib/template/popover/popover.html"}}).directive("uibPopover",["$uibTooltip",function(e){return e("uibPopover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(e,t,n){function a(){return angular.isDefined(e.maxParam)?e.maxParam:n.max}var i=this,o=angular.isDefined(t.animate)?e.$parent.$eval(t.animate):n.animate;this.bars=[],e.max=a(),this.addBar=function(e,t,n){o||t.css({transition:"none"}),this.bars.push(e),e.max=a(),e.title=n&&angular.isDefined(n.title)?n.title:"progressbar",e.$watch("value",function(t){e.recalculatePercentage()}),e.recalculatePercentage=function(){var t=i.bars.reduce(function(e,t){return t.percent=+(100*t.value/t.max).toFixed(2),e+t.percent},0);t>100&&(e.percent-=t-100)},e.$on("$destroy",function(){t=null,i.removeBar(e)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1),this.bars.forEach(function(e){e.recalculatePercentage()})},e.$watch("maxParam",function(e){i.bars.forEach(function(e){e.max=a(),e.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{maxParam:"=?max"},templateUrl:"uib/template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"uib/template/progressbar/bar.html",link:function(e,t,n,a){a.addBar(e,t,n)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",maxParam:"=?max",type:"@"},templateUrl:"uib/template/progressbar/progressbar.html",link:function(e,t,n,a){a.addBar(e,angular.element(t.children()[0]),{title:n.title})}}}),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,enableReset:!0,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(e,t,n){var a={$setViewValue:angular.noop},i=this;this.init=function(i){a=i,a.$render=this.render,a.$formatters.push(function(e){return angular.isNumber(e)&&e<<0!==e&&(e=Math.round(e)),e}),this.stateOn=angular.isDefined(t.stateOn)?e.$parent.$eval(t.stateOn):n.stateOn,this.stateOff=angular.isDefined(t.stateOff)?e.$parent.$eval(t.stateOff):n.stateOff,this.enableReset=angular.isDefined(t.enableReset)?e.$parent.$eval(t.enableReset):n.enableReset;var o=angular.isDefined(t.titles)?e.$parent.$eval(t.titles):n.titles;this.titles=angular.isArray(o)&&o.length>0?o:n.titles;var r=angular.isDefined(t.ratingStates)?e.$parent.$eval(t.ratingStates):new Array(angular.isDefined(t.max)?e.$parent.$eval(t.max):n.max);e.range=this.buildTemplateObjects(r)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;n>t;t++)e[t]=angular.extend({index:t},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(t)},e[t]);return e},this.getTitle=function(e){return e>=this.titles.length?e+1:this.titles[e]},e.rate=function(t){if(!e.readonly&&t>=0&&t<=e.range.length){var n=i.enableReset&&a.$viewValue===t?0:t;a.$setViewValue(n),a.$render()}},e.enter=function(t){e.readonly||(e.value=t),e.onHover({value:t})},e.reset=function(){e.value=a.$viewValue,e.onLeave()},e.onKeydown=function(t){/(37|38|39|40)/.test(t.which)&&(t.preventDefault(),t.stopPropagation(),e.rate(e.value+(38===t.which||39===t.which?1:-1)))},this.render=function(){e.value=a.$viewValue,e.title=i.getTitle(e.value-1)}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],restrict:"A",scope:{readonly:"=?readOnly",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"uib/template/rating/rating.html",link:function(e,t,n,a){var i=a[0],o=a[1];i.init(o)}}}),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(e){function t(e){for(var t=0;tt.index?1:e.index0&&13>t:t>=0&&24>t;return n&&""!==e.hours?(e.showMeridian&&(12===t&&(t=0),e.meridian===x[1]&&(t+=12)),t):void 0}function s(){var t=+e.minutes,n=t>=0&&60>t;return n&&""!==e.minutes?t:void 0}function u(){var t=+e.seconds;return t>=0&&60>t?t:void 0}function p(e,t){return null===e?"":angular.isDefined(e)&&e.toString().length<2&&!t?"0"+e:e.toString()}function c(e){d(),D.$setViewValue(new Date(w)),m(e)}function d(){v&&v.$setValidity("hours",!0),$&&$.$setValidity("minutes",!0),y&&y.$setValidity("seconds",!0),D.$setValidity("time",!0),e.invalidHours=!1,e.invalidMinutes=!1,e.invalidSeconds=!1}function m(t){if(D.$modelValue){var n=w.getHours(),a=w.getMinutes(),i=w.getSeconds();e.showMeridian&&(n=0===n||12===n?12:n%12),e.hours="h"===t?n:p(n,!C),"m"!==t&&(e.minutes=p(a)),e.meridian=w.getHours()<12?x[0]:x[1],"s"!==t&&(e.seconds=p(i)),e.meridian=w.getHours()<12?x[0]:x[1]}else e.hours=null,e.minutes=null,e.seconds=null,e.meridian=x[0]}function f(e){w=g(w,e),c()}function h(e,t){return g(e,60*t)}function g(e,t){var n=new Date(e.getTime()+1e3*t),a=new Date(e);return a.setHours(n.getHours(),n.getMinutes(),n.getSeconds()),a}function b(){return(null===e.hours||""===e.hours)&&(null===e.minutes||""===e.minutes)&&(!e.showSeconds||e.showSeconds&&(null===e.seconds||""===e.seconds))}var v,$,y,w=new Date,k=[],D={$setViewValue:angular.noop},x=angular.isDefined(n.meridians)?e.$parent.$eval(n.meridians):r.meridians||o.DATETIME_FORMATS.AMPMS,C=angular.isDefined(n.padHours)?e.$parent.$eval(n.padHours):!0;e.tabindex=angular.isDefined(n.tabindex)?n.tabindex:0,t.removeAttr("tabindex"),this.init=function(t,a){D=t,D.$render=this.render,D.$formatters.unshift(function(e){return e?new Date(e):null});var i=a.eq(0),o=a.eq(1),l=a.eq(2);v=i.controller("ngModel"),$=o.controller("ngModel"),y=l.controller("ngModel");var s=angular.isDefined(n.mousewheel)?e.$parent.$eval(n.mousewheel):r.mousewheel;s&&this.setupMousewheelEvents(i,o,l);var u=angular.isDefined(n.arrowkeys)?e.$parent.$eval(n.arrowkeys):r.arrowkeys;u&&this.setupArrowkeyEvents(i,o,l),e.readonlyInput=angular.isDefined(n.readonlyInput)?e.$parent.$eval(n.readonlyInput):r.readonlyInput,this.setupInputEvents(i,o,l)};var M=r.hourStep;n.hourStep&&k.push(e.$parent.$watch(a(n.hourStep),function(e){M=+e}));var T=r.minuteStep;n.minuteStep&&k.push(e.$parent.$watch(a(n.minuteStep),function(e){T=+e}));var O;k.push(e.$parent.$watch(a(n.min),function(e){var t=new Date(e);O=isNaN(t)?void 0:t}));var S;k.push(e.$parent.$watch(a(n.max),function(e){var t=new Date(e);S=isNaN(t)?void 0:t}));var P=!1;n.ngDisabled&&k.push(e.$parent.$watch(a(n.ngDisabled),function(e){P=e})),e.noIncrementHours=function(){var e=h(w,60*M);return P||e>S||w>e&&O>e},e.noDecrementHours=function(){var e=h(w,60*-M);return P||O>e||e>w&&e>S},e.noIncrementMinutes=function(){var e=h(w,T);return P||e>S||w>e&&O>e},e.noDecrementMinutes=function(){var e=h(w,-T);return P||O>e||e>w&&e>S},e.noIncrementSeconds=function(){var e=g(w,E);return P||e>S||w>e&&O>e},e.noDecrementSeconds=function(){var e=g(w,-E);return P||O>e||e>w&&e>S},e.noToggleMeridian=function(){return w.getHours()<12?P||h(w,720)>S:P||h(w,-720)0};t.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementHours():e.decrementHours()),t.preventDefault()}),n.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementMinutes():e.decrementMinutes()),t.preventDefault()}),a.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementSeconds():e.decrementSeconds()),t.preventDefault()})},this.setupArrowkeyEvents=function(t,n,a){t.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementHours(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementHours(),e.$apply()))}),n.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementMinutes(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementMinutes(),e.$apply()))}),a.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementSeconds(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementSeconds(),e.$apply()))})},this.setupInputEvents=function(t,n,a){if(e.readonlyInput)return e.updateHours=angular.noop,e.updateMinutes=angular.noop,void(e.updateSeconds=angular.noop);var i=function(t,n,a){D.$setViewValue(null),D.$setValidity("time",!1),angular.isDefined(t)&&(e.invalidHours=t,v&&v.$setValidity("hours",!1)),angular.isDefined(n)&&(e.invalidMinutes=n,$&&$.$setValidity("minutes",!1)),angular.isDefined(a)&&(e.invalidSeconds=a,y&&y.$setValidity("seconds",!1))};e.updateHours=function(){var e=l(),t=s();D.$setDirty(),angular.isDefined(e)&&angular.isDefined(t)?(w.setHours(e),w.setMinutes(t),O>w||w>S?i(!0):c("h")):i(!0)},t.on("blur",function(t){D.$setTouched(),b()?d():null===e.hours||""===e.hours?i(!0):!e.invalidHours&&e.hours<10&&e.$apply(function(){e.hours=p(e.hours,!C)})}),e.updateMinutes=function(){var e=s(),t=l();D.$setDirty(),angular.isDefined(e)&&angular.isDefined(t)?(w.setHours(t),w.setMinutes(e),O>w||w>S?i(void 0,!0):c("m")):i(void 0,!0)},n.on("blur",function(t){D.$setTouched(),b()?d():null===e.minutes?i(void 0,!0):!e.invalidMinutes&&e.minutes<10&&e.$apply(function(){e.minutes=p(e.minutes)})}),e.updateSeconds=function(){var e=u();D.$setDirty(),angular.isDefined(e)?(w.setSeconds(e),c("s")):i(void 0,void 0,!0)},a.on("blur",function(t){b()?d():!e.invalidSeconds&&e.seconds<10&&e.$apply(function(){e.seconds=p(e.seconds)})})},this.render=function(){var t=D.$viewValue;isNaN(t)?(D.$setValidity("time",!1),i.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(t&&(w=t),O>w||w>S?(D.$setValidity("time",!1),e.invalidHours=!0,e.invalidMinutes=!0):d(),m())},e.showSpinners=angular.isDefined(n.showSpinners)?e.$parent.$eval(n.showSpinners):r.showSpinners,e.incrementHours=function(){e.noIncrementHours()||f(60*M*60)},e.decrementHours=function(){e.noDecrementHours()||f(60*-M*60)},e.incrementMinutes=function(){e.noIncrementMinutes()||f(60*T)},e.decrementMinutes=function(){e.noDecrementMinutes()||f(60*-T)},e.incrementSeconds=function(){e.noIncrementSeconds()||f(E)},e.decrementSeconds=function(){e.noDecrementSeconds()||f(-E)},e.toggleMeridian=function(){var t=s(),n=l();e.noToggleMeridian()||(angular.isDefined(t)&&angular.isDefined(n)?f(720*(w.getHours()<12?60:-60)):e.meridian=e.meridian===x[0]?x[1]:x[0])},e.blur=function(){D.$setTouched()},e.$on("$destroy",function(){for(;k.length;)k.shift()()})}]).directive("uibTimepicker",["uibTimepickerConfig",function(e){return{require:["uibTimepicker","?^ngModel"],restrict:"A",controller:"UibTimepickerController",controllerAs:"timepicker",scope:{},templateUrl:function(t,n){return n.templateUrl||e.templateUrl},link:function(e,t,n,a){var i=a[0],o=a[1];o&&i.init(o,t.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.debounce","ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(e){var t=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(n){var a=n.match(t);if(!a)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+n+'".');return{itemName:a[3],source:e(a[4]),viewMapper:e(a[2]||a[1]),modelMapper:e(a[1])}}}}]).controller("UibTypeaheadController",["$scope","$element","$attrs","$compile","$parse","$q","$timeout","$document","$window","$rootScope","$$debounce","$uibPosition","uibTypeaheadParser",function(e,t,n,a,i,o,r,l,s,u,p,c,d){function m(){L.moveInProgress||(L.moveInProgress=!0,L.$digest()),J()}function f(){L.position=E?c.offset(t):c.position(t),L.position.top+=t.prop("offsetHeight")}function h(e){var t;return angular.version.minor<6?(t=e.$options||{},t.getOption=function(e){return t[e]}):t=e.$options,t}var g,b,v=[9,13,27,38,40],$=200,y=e.$eval(n.typeaheadMinLength);y||0===y||(y=1),e.$watch(n.typeaheadMinLength,function(e){y=e||0===e?e:1});var w=e.$eval(n.typeaheadWaitMs)||0,k=e.$eval(n.typeaheadEditable)!==!1;e.$watch(n.typeaheadEditable,function(e){k=e!==!1});var D,x,C=i(n.typeaheadLoading).assign||angular.noop,M=n.typeaheadShouldSelect?i(n.typeaheadShouldSelect):function(e,t){var n=t.$event;return 13===n.which||9===n.which},T=i(n.typeaheadOnSelect),O=angular.isDefined(n.typeaheadSelectOnBlur)?e.$eval(n.typeaheadSelectOnBlur):!1,S=i(n.typeaheadNoResults).assign||angular.noop,P=n.typeaheadInputFormatter?i(n.typeaheadInputFormatter):void 0,E=n.typeaheadAppendToBody?e.$eval(n.typeaheadAppendToBody):!1,A=n.typeaheadAppendTo?e.$eval(n.typeaheadAppendTo):null,I=e.$eval(n.typeaheadFocusFirst)!==!1,U=n.typeaheadSelectOnExact?e.$eval(n.typeaheadSelectOnExact):!1,N=i(n.typeaheadIsOpen).assign||angular.noop,F=e.$eval(n.typeaheadShowHint)||!1,H=i(n.ngModel),R=i(n.ngModel+"($$$p)"),V=function(t,n){return angular.isFunction(H(e))&&b.getOption("getterSetter")?R(t,{$$$p:n}):H.assign(t,n)},q=d.parse(n.uibTypeahead),L=e.$new(),z=e.$on("$destroy",function(){L.$destroy()});L.$on("$destroy",z);var Y="typeahead-"+L.$id+"-"+Math.floor(1e4*Math.random());t.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":Y});var B,W;F&&(B=angular.element("
        "),B.css("position","relative"),t.after(B),W=t.clone(),W.attr("placeholder",""),W.attr("tabindex","-1"),W.val(""),W.css({position:"absolute",top:"0px",left:"0px","border-color":"transparent","box-shadow":"none",opacity:1,background:"none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)",color:"#999"}),t.css({position:"relative","vertical-align":"top","background-color":"transparent"}),W.attr("id")&&W.removeAttr("id"),B.append(W),W.after(t));var j=angular.element("
        ");j.attr({id:Y,matches:"matches",active:"activeIdx",select:"select(activeIdx, evt)","move-in-progress":"moveInProgress",query:"query",position:"position","assign-is-open":"assignIsOpen(isOpen)",debounce:"debounceUpdate"}),angular.isDefined(n.typeaheadTemplateUrl)&&j.attr("template-url",n.typeaheadTemplateUrl),angular.isDefined(n.typeaheadPopupTemplateUrl)&&j.attr("popup-template-url",n.typeaheadPopupTemplateUrl);var _=function(){F&&W.val("")},G=function(){L.matches=[],L.activeIdx=-1,t.attr("aria-expanded",!1),_()},K=function(e){return Y+"-option-"+e};L.$watch("activeIdx",function(e){0>e?t.removeAttr("aria-activedescendant"):t.attr("aria-activedescendant",K(e))});var X=function(e,t){return L.matches.length>t&&e?e.toUpperCase()===L.matches[t].label.toUpperCase():!1},Z=function(n,a){var i={$viewValue:n};C(e,!0),S(e,!1),o.when(q.source(e,i)).then(function(o){var r=n===g.$viewValue;if(r&&D)if(o&&o.length>0){L.activeIdx=I?0:-1,S(e,!1),L.matches.length=0;for(var l=0;l0&&s.slice(0,n.length).toUpperCase()===n.toUpperCase()?W.val(n+s.slice(n.length)):W.val("")}}else G(),S(e,!0);r&&C(e,!1)},function(){G(),C(e,!1),S(e,!0)})};E&&(angular.element(s).on("resize",m),l.find("body").on("scroll",m));var J=p(function(){L.matches.length&&f(),L.moveInProgress=!1},$);L.moveInProgress=!1,L.query=void 0;var Q,ee=function(e){Q=r(function(){Z(e)},w)},te=function(){Q&&r.cancel(Q)};G(),L.assignIsOpen=function(t){N(e,t)},L.select=function(a,i){var o,l,s={};x=!0,s[q.itemName]=l=L.matches[a].model,o=q.modelMapper(e,s),V(e,o),g.$setValidity("editable",!0),g.$setValidity("parse",!0),T(e,{$item:l,$model:o,$label:q.viewMapper(e,s),$event:i}),G(),L.$eval(n.typeaheadFocusOnSelect)!==!1&&r(function(){t[0].focus()},0,!1)},t.on("keydown",function(t){if(0!==L.matches.length&&-1!==v.indexOf(t.which)){var n=M(e,{$event:t});if(-1===L.activeIdx&&n||9===t.which&&t.shiftKey)return G(),void L.$digest();t.preventDefault();var a;switch(t.which){case 27:t.stopPropagation(),G(),e.$digest();break;case 38:L.activeIdx=(L.activeIdx>0?L.activeIdx:L.matches.length)-1,L.$digest(),a=j[0].querySelectorAll(".uib-typeahead-match")[L.activeIdx], + a.parentNode.scrollTop=a.offsetTop;break;case 40:L.activeIdx=(L.activeIdx+1)%L.matches.length,L.$digest(),a=j[0].querySelectorAll(".uib-typeahead-match")[L.activeIdx],a.parentNode.scrollTop=a.offsetTop;break;default:n&&L.$apply(function(){angular.isNumber(L.debounceUpdate)||angular.isObject(L.debounceUpdate)?p(function(){L.select(L.activeIdx,t)},angular.isNumber(L.debounceUpdate)?L.debounceUpdate:L.debounceUpdate["default"]):L.select(L.activeIdx,t)})}}}),t.on("focus",function(e){D=!0,0!==y||g.$viewValue||r(function(){Z(g.$viewValue,e)},0)}),t.on("blur",function(e){O&&L.matches.length&&-1!==L.activeIdx&&!x&&(x=!0,L.$apply(function(){angular.isObject(L.debounceUpdate)&&angular.isNumber(L.debounceUpdate.blur)?p(function(){L.select(L.activeIdx,e)},L.debounceUpdate.blur):L.select(L.activeIdx,e)})),!k&&g.$error.editable&&(g.$setViewValue(),L.$apply(function(){g.$setValidity("editable",!0),g.$setValidity("parse",!0)}),t.val("")),D=!1,x=!1});var ne=function(n){t[0]!==n.target&&3!==n.which&&0!==L.matches.length&&(G(),u.$$phase||e.$digest())};l.on("click",ne),e.$on("$destroy",function(){l.off("click",ne),(E||A)&&ae.remove(),E&&(angular.element(s).off("resize",m),l.find("body").off("scroll",m)),j.remove(),F&&B.remove()});var ae=a(j)(L);E?l.find("body").append(ae):A?angular.element(A).eq(0).append(ae):t.after(ae),this.init=function(t){g=t,b=h(g),L.debounceUpdate=i(b.getOption("debounce"))(e),g.$parsers.unshift(function(t){return D=!0,0===y||t&&t.length>=y?w>0?(te(),ee(t)):Z(t):(C(e,!1),te(),G()),k?t:t?void g.$setValidity("editable",!1):(g.$setValidity("editable",!0),null)}),g.$formatters.push(function(t){var n,a,i={};return k||g.$setValidity("editable",!0),P?(i.$model=t,P(e,i)):(i[q.itemName]=t,n=q.viewMapper(e,i),i[q.itemName]=void 0,a=q.viewMapper(e,i),n!==a?n:t)})}}]).directive("uibTypeahead",function(){return{controller:"UibTypeaheadController",require:["ngModel","uibTypeahead"],link:function(e,t,n,a){a[1].init(a[0])}}}).directive("uibTypeaheadPopup",["$$debounce",function(e){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&",assignIsOpen:"&",debounce:"&"},replace:!0,templateUrl:function(e,t){return t.popupTemplateUrl||"uib/template/typeahead/typeahead-popup.html"},link:function(t,n,a){t.templateUrl=a.templateUrl,t.isOpen=function(){var e=t.matches.length>0;return t.assignIsOpen({isOpen:e}),e},t.isActive=function(e){return t.active===e},t.selectActive=function(e){t.active=e},t.selectMatch=function(n,a){var i=t.debounce();angular.isNumber(i)||angular.isObject(i)?e(function(){t.select({activeIdx:n,evt:a})},angular.isNumber(i)?i:i["default"]):t.select({activeIdx:n,evt:a})}}}}]).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(e,t,n){return{scope:{index:"=",match:"=",query:"="},link:function(a,i,o){var r=n(o.templateUrl)(a.$parent)||"uib/template/typeahead/typeahead-match.html";e(r).then(function(e){var n=angular.element(e.trim());i.replaceWith(n),t(n)(a)})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(e,t,n){function a(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function i(e){return/<.*>/g.test(e)}var o;return o=t.has("$sanitize"),function(t,r){return!o&&i(t)&&n.warn("Unsafe use of typeahead please use ngSanitize"),t=r?(""+t).replace(new RegExp(a(r),"gi"),"$&"):t,o||(t=e.trustAsHtml(t)),t}}]),angular.module("uib/template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("uib/template/accordion/accordion-group.html",'\n
        \n
        \n
        \n')}]),angular.module("uib/template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("uib/template/accordion/accordion.html",'
        ')}]),angular.module("uib/template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("uib/template/alert/alert.html",'\n
        \n')}]),angular.module("uib/template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("uib/template/carousel/carousel.html",'\n\n \n previous\n\n\n \n next\n\n\n')}]),angular.module("uib/template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("uib/template/carousel/slide.html",'
        \n')}]),angular.module("uib/template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("uib/template/datepicker/datepicker.html",'
        \n
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/datepicker/day.html",[]).run(["$templateCache",function(e){e.put("uib/template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
        {{::label.abbr}}
        {{ weekNumbers[$index] }}\n \n
        \n')}]),angular.module("uib/template/datepicker/month.html",[]).run(["$templateCache",function(e){e.put("uib/template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("uib/template/datepicker/year.html",[]).run(["$templateCache",function(e){e.put("uib/template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("uib/template/datepickerPopup/popup.html",[]).run(["$templateCache",function(e){e.put("uib/template/datepickerPopup/popup.html",'\n')}]),angular.module("uib/template/modal/window.html",[]).run(["$templateCache",function(e){e.put("uib/template/modal/window.html","
        \n")}]),angular.module("uib/template/pager/pager.html",[]).run(["$templateCache",function(e){e.put("uib/template/pager/pager.html",'
      • {{::getText(\'previous\')}}
      • \n
      • {{::getText(\'next\')}}
      • \n')}]),angular.module("uib/template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("uib/template/pagination/pagination.html",'\n\n\n\n\n')}]),angular.module("uib/template/tooltip/tooltip-html-popup.html",[]).run(["$templateCache",function(e){e.put("uib/template/tooltip/tooltip-html-popup.html",'
        \n
        \n')}]),angular.module("uib/template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("uib/template/tooltip/tooltip-popup.html",'
        \n
        \n')}]),angular.module("uib/template/tooltip/tooltip-template-popup.html",[]).run(["$templateCache",function(e){e.put("uib/template/tooltip/tooltip-template-popup.html",'
        \n
        \n')}]),angular.module("uib/template/popover/popover-html.html",[]).run(["$templateCache",function(e){e.put("uib/template/popover/popover-html.html",'
        \n\n
        \n

        \n
        \n
        \n')}]),angular.module("uib/template/popover/popover-template.html",[]).run(["$templateCache",function(e){e.put("uib/template/popover/popover-template.html",'
        \n\n
        \n

        \n
        \n
        \n')}]),angular.module("uib/template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("uib/template/popover/popover.html",'
        \n\n
        \n

        \n
        \n
        \n')}]),angular.module("uib/template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("uib/template/progressbar/bar.html",'
        \n')}]),angular.module("uib/template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("uib/template/progressbar/progress.html",'
        ')}]),angular.module("uib/template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("uib/template/progressbar/progressbar.html",'
        \n
        \n
        \n')}]),angular.module("uib/template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("uib/template/rating/rating.html",'\n ({{ $index < value ? \'*\' : \' \' }})\n \n\n')}]),angular.module("uib/template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("uib/template/tabs/tab.html",'\n')}]),angular.module("uib/template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("uib/template/tabs/tabset.html",'
        \n \n
        \n
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("uib/template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
          
        \n \n :\n \n :\n \n
          
        \n')}]),angular.module("uib/template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("uib/template/typeahead/typeahead-match.html",'\n')}]),angular.module("uib/template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("uib/template/typeahead/typeahead-popup.html",'\n')}]),angular.module("ui.bootstrap.carousel").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibCarouselCss&&angular.element(document).find("head").prepend(''),angular.$$uibCarouselCss=!0}),angular.module("ui.bootstrap.datepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerCss=!0}),angular.module("ui.bootstrap.position").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibPositionCss&&angular.element(document).find("head").prepend(''),angular.$$uibPositionCss=!0}),angular.module("ui.bootstrap.datepickerPopup").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerpopupCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerpopupCss=!0}),angular.module("ui.bootstrap.tooltip").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTooltipCss&&angular.element(document).find("head").prepend(''),angular.$$uibTooltipCss=!0}),angular.module("ui.bootstrap.timepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTimepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibTimepickerCss=!0}),angular.module("ui.bootstrap.typeahead").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTypeaheadCss&&angular.element(document).find("head").prepend(''),angular.$$uibTypeaheadCss=!0}); \ No newline at end of file diff --git a/ui-bootstrap.js b/ui-bootstrap.js index 43c0f03..60c6881 100644 --- a/ui-bootstrap.js +++ b/ui-bootstrap.js @@ -2,51 +2,133 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.13.1-SNAPSHOT - 2015-10-06 + * Version: 2.5.0 - 2017-01-28 * License: MIT - */ -angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.transition","ui.bootstrap.typeahead"]); + */angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); angular.module('ui.bootstrap.collapse', []) - .directive('collapse', ['$animate', function ($animate) { - + .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { + var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + var expandingExpr = $parse(attrs.expanding), + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed), + horizontal = false, + css = {}, + cssTo = {}; + + init(); + + function init() { + horizontal = !!('horizontal' in attrs); + if (horizontal) { + css = { + width: '' + }; + cssTo = {width: '0'}; + } else { + css = { + height: '' + }; + cssTo = {height: '0'}; + } + if (!scope.$eval(attrs.uibCollapse)) { + element.addClass('in') + .addClass('collapse') + .attr('aria-expanded', true) + .attr('aria-hidden', false) + .css(css); + } + } + + function getScrollFromElement(element) { + if (horizontal) { + return {width: element.scrollWidth + 'px'}; + } + return {height: element.scrollHeight + 'px'}; + } + function expand() { - element.removeClass('collapse').addClass('collapsing'); - $animate.addClass(element, 'in', { - to: { height: element[0].scrollHeight + 'px' } - }).then(expandDone); + if (element.hasClass('collapse') && element.hasClass('in')) { + return; + } + + $q.resolve(expandingExpr(scope)) + .then(function() { + element.removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', true) + .attr('aria-hidden', false); + + if ($animateCss) { + $animateCss(element, { + addClass: 'in', + easing: 'ease', + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) + }).start()['finally'](expandDone); + } else { + $animate.addClass(element, 'in', { + css: { + overflow: 'hidden' + }, + to: getScrollFromElement(element[0]) + }).then(expandDone); + } + }, angular.noop); } function expandDone() { - element.removeClass('collapsing'); - element.css({height: 'auto'}); + element.removeClass('collapsing') + .addClass('collapse') + .css(css); + expandedExpr(scope); } function collapse() { - element - // IMPORTANT: The height must be set before adding "collapsing" class. - // Otherwise, the browser attempts to animate from height 0 (in - // collapsing class) to the given height here. - .css({height: element[0].scrollHeight + 'px'}) - // initially all panel collapse have the collapse class, this removal - // prevents the animation from jumping to collapsed state - .removeClass('collapse') - .addClass('collapsing'); + if (!element.hasClass('collapse') && !element.hasClass('in')) { + return collapseDone(); + } - $animate.removeClass(element, 'in', { - to: {height: '0'} - }).then(collapseDone); + $q.resolve(collapsingExpr(scope)) + .then(function() { + element + // IMPORTANT: The width must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from width 0 (in + // collapsing class) to the given width here. + .css(getScrollFromElement(element[0])) + // initially all panel collapse have the collapse class, this removal + // prevents the animation from jumping to collapsed state + .removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', false) + .attr('aria-hidden', true); + + if ($animateCss) { + $animateCss(element, { + removeClass: 'in', + to: cssTo + }).start()['finally'](collapseDone); + } else { + $animate.removeClass(element, 'in', { + to: cssTo + }).then(collapseDone); + } + }, angular.noop); } function collapseDone() { - element.css({height: '0'}); // Required so that collapse works when animation is disabled - element.removeClass('collapsing'); - element.addClass('collapse'); + element.css(cssTo); // Required so that collapse works when animation is disabled + element.removeClass('collapsing') + .addClass('collapse'); + collapsedExpr(scope); } - scope.$watch(attrs.collapse, function (shouldCollapse) { + scope.$watch(attrs.uibCollapse, function(shouldCollapse) { if (shouldCollapse) { collapse(); } else { @@ -57,23 +139,36 @@ angular.module('ui.bootstrap.collapse', []) }; }]); -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) +angular.module('ui.bootstrap.tabindex', []) + +.directive('uibTabindexToggle', function() { + return { + restrict: 'A', + link: function(scope, elem, attrs) { + attrs.$observe('disabled', function(disabled) { + attrs.$set('tabindex', disabled ? -1 : null); + }); + } + }; +}); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex']) -.constant('accordionConfig', { +.constant('uibAccordionConfig', { closeOthers: true }) -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - +.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? + $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if (closeOthers) { + angular.forEach(this.groups, function(group) { + if (group !== openGroup) { group.isOpen = false; } }); @@ -85,7 +180,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) var that = this; this.groups.push(groupScope); - groupScope.$on('$destroy', function (event) { + groupScope.$on('$destroy', function(event) { that.removeGroup(groupScope); }); }; @@ -93,35 +188,37 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); - if ( index !== -1 ) { + if (index !== -1) { this.groups.splice(index, 1); } }; - }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. -.directive('accordion', function () { +.directive('uibAccordion', function() { return { - restrict:'EA', - controller:'AccordionController', + controller: 'UibAccordionController', + controllerAs: 'accordion', transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion.html'; + } }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', function() { +.directive('uibAccordionGroup', function() { return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', + require: '^uibAccordion', // We need this directive to be inside an accordion + transclude: true, // It transcludes the contents of the directive into the template + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; + }, scope: { heading: '@', // Interpolate the heading attribute onto this scope + panelClass: '@?', // Ditto with panelClass isOpen: '=?', isDisabled: '=?' }, @@ -131,35 +228,41 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) }; }, link: function(scope, element, attrs, accordionCtrl) { + element.addClass('panel'); accordionCtrl.addGroup(scope); + scope.openClass = attrs.openClass || 'panel-open'; + scope.panelClass = attrs.panelClass || 'panel-default'; scope.$watch('isOpen', function(value) { - if ( value ) { + element.toggleClass(scope.openClass, !!value); + if (value) { accordionCtrl.closeOthers(scope); } }); - scope.toggleOpen = function() { - if ( !scope.isDisabled ) { - scope.isOpen = !scope.isOpen; + scope.toggleOpen = function($event) { + if (!scope.isDisabled) { + if (!$event || $event.which === 32) { + scope.isOpen = !scope.isOpen; + } } }; + + var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + scope.headingId = id + '-tab'; + scope.panelId = id + '-panel'; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { +.directive('uibAccordionHeading', function() { return { - restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, - require: '^accordionGroup', - link: function(scope, element, attr, accordionGroupCtrl, transclude) { + require: '^uibAccordionGroup', + link: function(scope, element, attrs, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] @@ -170,114 +273,129 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element -//
        -// -// ... -//
        -.directive('accordionTransclude', function() { +.directive('uibAccordionTransclude', function() { return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); + require: '^uibAccordionGroup', + link: function(scope, element, attrs, controller) { + scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { + if (heading) { + var elem = angular.element(element[0].querySelector(getHeaderSelectors())); + elem.html(''); + elem.append(heading); } }); } }; -}) -; + function getHeaderSelectors() { + return 'uib-accordion-header,' + + 'data-uib-accordion-header,' + + 'x-uib-accordion-header,' + + 'uib\\:accordion-header,' + + '[uib-accordion-header],' + + '[data-uib-accordion-header],' + + '[x-uib-accordion-header]'; + } +}); angular.module('ui.bootstrap.alert', []) -.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { - $scope.closeable = 'close' in $attrs; - this.close = $scope.close; +.controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) { + $scope.closeable = !!$attrs.close; + $element.addClass('alert'); + $attrs.$set('role', 'alert'); + if ($scope.closeable) { + $element.addClass('alert-dismissible'); + } + + var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? + $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; + + if (dismissOnTimeout) { + $timeout(function() { + $scope.close(); + }, parseInt(dismissOnTimeout, 10)); + } }]) -.directive('alert', function () { +.directive('uibAlert', function() { return { - restrict:'EA', - controller:'AlertController', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, + controller: 'UibAlertController', + controllerAs: 'alert', + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/alert/alert.html'; + }, + transclude: true, scope: { - type: '@', close: '&' } }; -}) - -.directive('dismissOnTimeout', ['$timeout', function($timeout) { - return { - require: 'alert', - link: function(scope, element, attrs, alertCtrl) { - $timeout(function(){ - alertCtrl.close(); - }, parseInt(attrs.dismissOnTimeout, 10)); - } - }; -}]); - -angular.module('ui.bootstrap.bindHtml', []) +}); - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); angular.module('ui.bootstrap.buttons', []) -.constant('buttonConfig', { +.constant('uibButtonConfig', { activeClass: 'active', toggleEvent: 'click' }) -.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { +.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) -.directive('btnRadio', function () { +.directive('uibBtnRadio', ['$parse', function($parse) { return { - require: ['btnRadio', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnRadio', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'buttons', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + var uncheckableExpr = $parse(attrs.uibUncheckable); + + element.find('input').css({display: 'none'}); //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + ngModelCtrl.$render = function() { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio))); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + var isActive = element.hasClass(buttonsCtrl.activeClass); if (!isActive || angular.isDefined(attrs.uncheckable)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + scope.$apply(function() { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio)); ngModelCtrl.$render(); }); } }); + + if (attrs.uibUncheckable) { + scope.$watch(uncheckableExpr, function(uncheckable) { + attrs.$set('uncheckable', uncheckable ? '' : undefined); + }); + } } }; -}) +}]) -.directive('btnCheckbox', function () { +.directive('uibBtnCheckbox', function() { return { - require: ['btnCheckbox', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnCheckbox', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'button', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + element.find('input').css({display: 'none'}); + function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } @@ -286,19 +404,22 @@ angular.module('ui.bootstrap.buttons', []) return getCheckboxValue(attrs.btnCheckboxFalse, false); } - function getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); - return angular.isDefined(val) ? val : defaultValue; + function getCheckboxValue(attribute, defaultValue) { + return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; } //model -> UI - ngModelCtrl.$render = function () { + ngModelCtrl.$render = function() { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - scope.$apply(function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + + scope.$apply(function() { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); @@ -307,118 +428,237 @@ angular.module('ui.bootstrap.buttons', []) }; }); -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ angular.module('ui.bootstrap.carousel', []) -.controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) { + +.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) { var self = this, slides = self.slides = $scope.slides = [], - NO_TRANSITION = 'uib-noTransition', SLIDE_DIRECTION = 'uib-slideDirection', - currentIndex = -1, + currentIndex = $scope.active, currentInterval, isPlaying; - self.currentSlide = null; var destroyed = false; + $element.addClass('carousel'); + + self.addSlide = function(slide, element) { + slides.push({ + slide: slide, + element: element + }); + slides.sort(function(a, b) { + return +a.slide.index - +b.slide.index; + }); + //if this is the first slide or the slide is set to active, select it + if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) { + if ($scope.$currentTransition) { + $scope.$currentTransition = null; + } + + currentIndex = slide.index; + $scope.active = slide.index; + setActive(currentIndex); + self.select(slides[findSlideIndex(slide)]); + if (slides.length === 1) { + $scope.play(); + } + } + }; + + self.getCurrentIndex = function() { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === currentIndex) { + return i; + } + } + }; + + self.next = $scope.next = function() { + var newIndex = (self.getCurrentIndex() + 1) % slides.length; + + if (newIndex === 0 && $scope.noWrap()) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'next'); + }; + + self.prev = $scope.prev = function() { + var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + + if ($scope.noWrap() && newIndex === slides.length - 1) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'prev'); + }; + + self.removeSlide = function(slide) { + var index = findSlideIndex(slide); + + //get the index of the slide inside the carousel + slides.splice(index, 1); + if (slides.length > 0 && currentIndex === index) { + if (index >= slides.length) { + currentIndex = slides.length - 1; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[slides.length - 1]); + } else { + currentIndex = index; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + $scope.active = currentIndex; + } + + //clean the active value when no more slide + if (slides.length === 0) { + currentIndex = null; + $scope.active = null; + } + }; + /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = self.indexOfSlide(nextSlide); + var nextIndex = findSlideIndex(nextSlide.slide); //Decide direction if it's not given if (direction === undefined) { direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; } //Prevent this user-triggered transition from occurring if there is already one in progress - if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) { - goNext(nextSlide, nextIndex, direction); + if (nextSlide.slide.index !== currentIndex && + !$scope.$currentTransition) { + goNext(nextSlide.slide, nextIndex, direction); } }; - function goNext(slide, index, direction) { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - - angular.extend(slide, {direction: direction, active: true}); - angular.extend(self.currentSlide || {}, {direction: direction, active: false}); - if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition && - slide.$element) { - slide.$element.data(SLIDE_DIRECTION, slide.direction); - $scope.$currentTransition = true; - slide.$element.one('$animate:close', function closeFn() { - $scope.$currentTransition = null; - }); - } + /* Allow outside people to call indexOf on slides array */ + $scope.indexOfSlide = function(slide) { + return +slide.slide.index; + }; - self.currentSlide = slide; - currentIndex = index; + $scope.isActive = function(slide) { + return $scope.active === slide.slide.index; + }; - // every time you change the index, prefetch adjacent slides - // $scope.$evalAsync(self.preFetchSlides); - $scope.$evalAsync(function () { - angular.extend(getSlideByIndex(self.getCurrentIndex()),{fetch: true}); - }); + $scope.isPrevDisabled = function() { + return $scope.active === 0 && $scope.noWrap(); + }; - //every time you change slides, reset the timer - restartTimer(); - } + $scope.isNextDisabled = function() { + return $scope.active === slides.length - 1 && $scope.noWrap(); + }; - $scope.$on('$destroy', function () { + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + + $element.on('mouseenter', $scope.pause); + $element.on('mouseleave', $scope.play); + + $scope.$on('$destroy', function() { destroyed = true; + resetTimer(); }); - function getSlideByIndex(index) { - if (angular.isUndefined(slides[index].index)) { - return slides[index]; + $scope.$watch('noTransition', function(noTransition) { + $animate.enabled($element, !noTransition); + }); + + $scope.$watch('interval', restartTimer); + + $scope.$watchCollection('slides', resetTransition); + + $scope.$watch('active', function(index) { + if (angular.isNumber(index) && currentIndex !== index) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === index) { + index = i; + break; + } + } + + var slide = slides[index]; + if (slide) { + setActive(index); + self.select(slides[index]); + currentIndex = index; + } } - var i, len = slides.length; - for (i = 0; i < slides.length; ++i) { - if (slides[i].index == index) { + }); + + function getSlideByIndex(index) { + for (var i = 0, l = slides.length; i < l; ++i) { + if (slides[i].index === index) { return slides[i]; } } } - self.getCurrentIndex = function() { - if (self.currentSlide && angular.isDefined(self.currentSlide.index)) { - return +self.currentSlide.index; + function setActive(index) { + for (var i = 0; i < slides.length; i++) { + slides[i].slide.active = i === index; } - return currentIndex; - }; + } - /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide); - }; + function goNext(slide, index, direction) { + if (destroyed) { + return; + } - $scope.next = function() { - var newIndex = (self.getCurrentIndex() + 1) % slides.length; + angular.extend(slide, {direction: direction}); + angular.extend(slides[currentIndex].slide || {}, {direction: direction}); + if ($animate.enabled($element) && !$scope.$currentTransition && + slides[index].element && self.slides.length > 1) { + slides[index].element.data(SLIDE_DIRECTION, slide.direction); + var currentIdx = self.getCurrentIndex(); - return self.select(getSlideByIndex(newIndex), 'next'); - }; + if (angular.isNumber(currentIdx) && slides[currentIdx].element) { + slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction); + } - $scope.prev = function() { - var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + $scope.$currentTransition = true; + $animate.on('addClass', slides[index].element, function(element, phase) { + if (phase === 'close') { + $scope.$currentTransition = null; + $animate.off('addClass', element); + } + }); + } - return self.select(getSlideByIndex(newIndex), 'prev'); - }; + $scope.active = slide.index; + currentIndex = slide.index; + setActive(index); - $scope.isActive = function(slide) { - return self.currentSlide === slide; - }; + // every time you change the index, prefetch adjacent slides + $scope.$evalAsync(function () { + angular.extend(getSlideByIndex(self.getCurrentIndex()),{fetch: true}); + }); - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); + //every time you change slides, reset the timer + restartTimer(); + } - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval > 0) { - currentInterval = $interval(timerFn, interval); + function findSlideIndex(slide) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide === slide) { + return i; + } } } @@ -429,6 +669,20 @@ angular.module('ui.bootstrap.carousel', []) } } + function resetTransition(slides) { + if (!slides.length) { + $scope.$currentTransition = null; + } + } + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + function timerFn() { var interval = +$scope.interval; if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { @@ -437,346 +691,419 @@ angular.module('ui.bootstrap.carousel', []) $scope.pause(); } } +}]) - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); +.directive('uibCarousel', function() { + return { + transclude: true, + controller: 'UibCarouselController', + controllerAs: 'carousel', + restrict: 'A', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/carousel.html'; + }, + scope: { + active: '=', + interval: '=', + noTransition: '=', + noPause: '=', + noWrap: '&' } }; - $scope.pause = function() { - if (!$scope.noPause) { - isPlaying = false; - resetTimer(); +}) + +.directive('uibSlide', ['$animate', function($animate) { + return { + require: '^uibCarousel', + restrict: 'A', + transclude: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/slide.html'; + }, + scope: { + actual: '=?', + index: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + element.addClass('item'); + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + $animate[active ? 'addClass' : 'removeClass'](element, 'active'); + }); } }; +}]) - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; - } - }; - - self.removeSlide = function(slide) { - if (angular.isDefined(slide.index)) { - slides.sort(function(a, b) { - return +a.index > +b.index; - }); - } - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { - self.select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - }; - - self.preFetchSlides = function () { - var fetchIndex = self.getCurrentIndex(); - var fwdIndex = (fetchIndex + 1) % slides.length; - var revIndex = fetchIndex - 1 < 0 ? slides.length - 1 : fetchIndex - 1; - angular.extend(getSlideByIndex(fetchIndex),{fetch: true}); - angular.extend(getSlideByIndex(fwdIndex),{fetch: true}); - angular.extend(getSlideByIndex(revIndex),{fetch: true}); - }; - - $scope.$watch('noTransition', function(noTransition) { - $element.data(NO_TRANSITION, noTransition); - }); - -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', - scope: { - interval: '=', - noTransition: '=', - noPause: '=' - } - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * @param {number=} index The index of the slide. The slides will be sorted by this parameter. - * - * @example - - -
        - - - - - - - Interval, in milliseconds: -
        Enter a negative number to stop the interval. -
        -
        - -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
        -*/ - -.directive('slide', function() { - return { - require: '^carousel', - restrict: 'EA', - transclude: true, - replace: true, - templateUrl: 'template/carousel/slide.html', - scope: { - active: '=?', - index: '=?', - fetch: '=?' - }, - link: function (scope, element, attrs, carouselCtrl) { - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('$destroy', function() { - carouselCtrl.removeSlide(scope); - }); +.animation('.item', ['$animateCss', +function($animateCss) { + var SLIDE_DIRECTION = 'uib-slideDirection'; - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); + function removeClass(element, className, callback) { + element.removeClass(className); + if (callback) { + callback(); } - }; -}) - -.animation('.item', [ - '$animate', -function ($animate) { - var NO_TRANSITION = 'uib-noTransition', - SLIDE_DIRECTION = 'uib-slideDirection'; + } return { - beforeAddClass: function (element, className, done) { - // Due to transclusion, noTransition property is on parent's scope - if (className == 'active' && element.parent() && - !element.parent().data(NO_TRANSITION)) { + beforeAddClass: function(element, className, done) { + if (className === 'active') { var stopped = false; var direction = element.data(SLIDE_DIRECTION); - var directionClass = direction == 'next' ? 'left' : 'right'; + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, + directionClass + ' ' + direction, done); element.addClass(direction); - $animate.addClass(element, directionClass).then(function () { - if (!stopped) { - element.removeClass(directionClass + ' ' + direction); - } - done(); - }); - return function () { + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { stopped = true; }; } done(); }, beforeRemoveClass: function (element, className, done) { - // Due to transclusion, noTransition property is on parent's scope - if (className == 'active' && element.parent() && - !element.parent().data(NO_TRANSITION)) { + if (className === 'active') { var stopped = false; var direction = element.data(SLIDE_DIRECTION); - var directionClass = direction == 'next' ? 'left' : 'right'; - $animate.addClass(element, directionClass).then(function () { - if (!stopped) { - element.removeClass(directionClass); - } - done(); - }); - return function () { + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, directionClass, done); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { stopped = true; }; } done(); } }; - -}]) - - -; +}]); angular.module('ui.bootstrap.dateparser', []) -.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) { // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; - this.parsers = {}; + var localeId; + var formatCodeToRegex; - var formatCodeToRegex = { - 'yyyy': { - regex: '\\d{4}', - apply: function(value) { this.year = +value; } - }, - 'yy': { - regex: '\\d{2}', - apply: function(value) { this.year = +value + 2000; } - }, - 'y': { - regex: '\\d{1,4}', - apply: function(value) { this.year = +value; } - }, - 'MMMM': { - regex: $locale.DATETIME_FORMATS.MONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } - }, - 'MMM': { - regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } - }, - 'MM': { - regex: '0[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'M': { - regex: '[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'dd': { - regex: '[0-2][0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'd': { - regex: '[1-2]?[0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'EEEE': { - regex: $locale.DATETIME_FORMATS.DAY.join('|') - }, - 'EEE': { - regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') - }, - 'HH': { - regex: '(?:0|1)[0-9]|2[0-3]', - apply: function(value) { this.hours = +value; } - }, - 'H': { - regex: '1?[0-9]|2[0-3]', - apply: function(value) { this.hours = +value; } - }, - 'mm': { - regex: '[0-5][0-9]', - apply: function(value) { this.minutes = +value; } - }, - 'm': { - regex: '[0-9]|[1-5][0-9]', - apply: function(value) { this.minutes = +value; } - }, - 'sss': { - regex: '[0-9][0-9][0-9]', - apply: function(value) { this.milliseconds = +value; } - }, - 'ss': { - regex: '[0-5][0-9]', - apply: function(value) { this.seconds = +value; } - }, - 's': { - regex: '[0-9]|[1-5][0-9]', - apply: function(value) { this.seconds = +value; } + this.init = function() { + localeId = $locale.id; + + this.parsers = {}; + this.formatters = {}; + + formatCodeToRegex = [ + { + key: 'yyyy', + regex: '\\d{4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yyyy'); + } + }, + { + key: 'yy', + regex: '\\d{2}', + apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yy'); + } + }, + { + key: 'y', + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'y'); + } + }, + { + key: 'M!', + regex: '0?[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { + var value = date.getMonth(); + if (/^[0-9]$/.test(value)) { + return dateFilter(date, 'MM'); + } + + return dateFilter(date, 'M'); + } + }, + { + key: 'MMMM', + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMMM'); } + }, + { + key: 'MMM', + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMM'); } + }, + { + key: 'MM', + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'MM'); } + }, + { + key: 'M', + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'M'); } + }, + { + key: 'd!', + regex: '[0-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { + var value = date.getDate(); + if (/^[1-9]$/.test(value)) { + return dateFilter(date, 'dd'); + } + + return dateFilter(date, 'd'); + } + }, + { + key: 'dd', + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'dd'); } + }, + { + key: 'd', + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'd'); } + }, + { + key: 'EEEE', + regex: $locale.DATETIME_FORMATS.DAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEEE'); } + }, + { + key: 'EEE', + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEE'); } + }, + { + key: 'HH', + regex: '(?:0|1)[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'HH'); } + }, + { + key: 'hh', + regex: '0[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'hh'); } + }, + { + key: 'H', + regex: '1?[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'H'); } + }, + { + key: 'h', + regex: '[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'h'); } + }, + { + key: 'mm', + regex: '[0-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'mm'); } + }, + { + key: 'm', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'm'); } + }, + { + key: 'sss', + regex: '[0-9][0-9][0-9]', + apply: function(value) { this.milliseconds = +value; }, + formatter: function(date) { return dateFilter(date, 'sss'); } + }, + { + key: 'ss', + regex: '[0-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 'ss'); } + }, + { + key: 's', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 's'); } + }, + { + key: 'a', + regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), + apply: function(value) { + if (this.hours === 12) { + this.hours = 0; + } + + if (value === 'PM') { + this.hours += 12; + } + }, + formatter: function(date) { return dateFilter(date, 'a'); } + }, + { + key: 'Z', + regex: '[+-]\\d{4}', + apply: function(value) { + var matches = value.match(/([+-])(\d{2})(\d{2})/), + sign = matches[1], + hours = matches[2], + minutes = matches[3]; + this.hours += toInt(sign + hours); + this.minutes += toInt(sign + minutes); + }, + formatter: function(date) { + return dateFilter(date, 'Z'); + } + }, + { + key: 'ww', + regex: '[0-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'ww'); } + }, + { + key: 'w', + regex: '[0-9]|[1-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'w'); } + }, + { + key: 'GGGG', + regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'), + formatter: function(date) { return dateFilter(date, 'GGGG'); } + }, + { + key: 'GGG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GGG'); } + }, + { + key: 'GG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GG'); } + }, + { + key: 'G', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'G'); } + } + ]; + + if (angular.version.major >= 1 && angular.version.minor > 4) { + formatCodeToRegex.push({ + key: 'LLLL', + regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'LLLL'); } + }); } }; + this.init(); + + function getFormatCodeToRegex(key) { + return filterFilter(formatCodeToRegex, {key: key}, true)[0]; + } + + this.getParser = function (key) { + var f = getFormatCodeToRegex(key); + return f && f.apply || null; + }; + + this.overrideParser = function (key, parser) { + var f = getFormatCodeToRegex(key); + if (f && angular.isFunction(parser)) { + this.parsers = {}; + f.apply = parser; + } + }.bind(this); + function createParser(format) { var map = [], regex = format.split(''); - angular.forEach(formatCodeToRegex, function(data, code) { - var index = format.indexOf(code); + // check for literal values + var quoteIndex = format.indexOf('\''); + if (quoteIndex > -1) { + var inLiteral = false; + format = format.split(''); + for (var i = quoteIndex; i < format.length; i++) { + if (inLiteral) { + if (format[i] === '\'') { + if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote + format[i+1] = '$'; + regex[i+1] = ''; + } else { // end of literal + regex[i] = ''; + inLiteral = false; + } + } + format[i] = '$'; + } else { + if (format[i] === '\'') { // start of literal + format[i] = '$'; + regex[i] = ''; + inLiteral = true; + } + } + } + + format = format.join(''); + } + + angular.forEach(formatCodeToRegex, function(data) { + var index = format.indexOf(data.key); if (index > -1) { format = format.split(''); regex[index] = '(' + data.regex + ')'; format[index] = '$'; // Custom symbol to define consumed part of format - for (var i = index + 1, n = index + code.length; i < n; i++) { + for (var i = index + 1, n = index + data.key.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); - map.push({ index: index, apply: data.apply }); + map.push({ + index: index, + key: data.key, + apply: data.apply, + matcher: data.regex + }); } }); @@ -786,26 +1113,116 @@ angular.module('ui.bootstrap.dateparser', []) }; } + function createFormatter(format) { + var formatters = []; + var i = 0; + var formatter, literalIdx; + while (i < format.length) { + if (angular.isNumber(literalIdx)) { + if (format.charAt(i) === '\'') { + if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') { + formatters.push(constructLiteralFormatter(format, literalIdx, i)); + literalIdx = null; + } + } else if (i === format.length) { + while (literalIdx < format.length) { + formatter = constructFormatterFromIdx(format, literalIdx); + formatters.push(formatter); + literalIdx = formatter.endIdx; + } + } + + i++; + continue; + } + + if (format.charAt(i) === '\'') { + literalIdx = i; + i++; + continue; + } + + formatter = constructFormatterFromIdx(format, i); + + formatters.push(formatter.parser); + i = formatter.endIdx; + } + + return formatters; + } + + function constructLiteralFormatter(format, literalIdx, endIdx) { + return function() { + return format.substr(literalIdx + 1, endIdx - literalIdx - 1); + }; + } + + function constructFormatterFromIdx(format, i) { + var currentPosStr = format.substr(i); + for (var j = 0; j < formatCodeToRegex.length; j++) { + if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) { + var data = formatCodeToRegex[j]; + return { + endIdx: i + data.key.length, + parser: data.formatter + }; + } + } + + return { + endIdx: i + 1, + parser: function() { + return currentPosStr.charAt(0); + } + }; + } + + this.filter = function(date, format) { + if (!angular.isDate(date) || isNaN(date) || !format) { + return ''; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.formatters[format]) { + this.formatters[format] = createFormatter(format); + } + + var formatters = this.formatters[format]; + + return formatters.reduce(function(str, formatter) { + return str + formatter(date); + }, ''); + }; + this.parse = function(input, format, baseDate) { - if ( !angular.isString(input) || !format ) { + if (!angular.isString(input) || !format) { return input; } format = $locale.DATETIME_FORMATS[format] || format; format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); - if ( !this.parsers[format] ) { - this.parsers[format] = createParser(format); + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.parsers[format]) { + this.parsers[format] = createParser(format, 'apply'); } var parser = this.parsers[format], regex = parser.regex, map = parser.map, - results = input.match(regex); - - if ( results && results.length ) { + results = input.match(regex), + tzOffset = false; + if (results && results.length) { var fields, dt; - if (baseDate) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { fields = { year: baseDate.getFullYear(), month: baseDate.getMonth(), @@ -816,19 +1233,40 @@ angular.module('ui.bootstrap.dateparser', []) milliseconds: baseDate.getMilliseconds() }; } else { + if (baseDate) { + $log.warn('dateparser:', 'baseDate is not a valid date'); + } fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; } - for( var i = 1, n = results.length; i < n; i++ ) { - var mapper = map[i-1]; - if ( mapper.apply ) { + for (var i = 1, n = results.length; i < n; i++) { + var mapper = map[i - 1]; + if (mapper.matcher === 'Z') { + tzOffset = true; + } + + if (mapper.apply) { mapper.apply.call(fields, results[i]); } } - if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds, - fields.milliseconds || 0); + var datesetter = tzOffset ? Date.prototype.setUTCFullYear : + Date.prototype.setFullYear; + var timesetter = tzOffset ? Date.prototype.setUTCHours : + Date.prototype.setHours; + + if (isValid(fields.year, fields.month, fields.date)) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) { + dt = new Date(baseDate); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours, fields.minutes, + fields.seconds, fields.milliseconds); + } else { + dt = new Date(0); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours || 0, fields.minutes || 0, + fields.seconds || 0, fields.milliseconds || 0); + } } return dt; @@ -842,230 +1280,304 @@ angular.module('ui.bootstrap.dateparser', []) return false; } - if ( month === 1 && date > 28) { - return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + if (month === 1 && date > 28) { + return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0); } - if ( month === 3 || month === 5 || month === 8 || month === 10) { - return date < 31; + if (month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; } return true; } -}]); -angular.module('ui.bootstrap.position', []) + function toInt(str) { + return parseInt(str, 10); + } -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { + this.toTimezone = toTimezone; + this.fromTimezone = fromTimezone; + this.timezoneToOffset = timezoneToOffset; + this.addDateMinutes = addDateMinutes; + this.convertTimezoneToLocal = convertTimezoneToLocal; - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } + function toTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone) : date; + } - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } + function fromTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date; + } - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; + //https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207 + function timezoneToOffset(timezone, fallback) { + timezone = timezone.replace(/:/g, ''); + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + } - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; - } + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; + } - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); + } +}]); - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, +// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to +// at most one element. +angular.module('ui.bootstrap.isClass', []) +.directive('uibIsClass', [ + '$animate', +function ($animate) { + // 11111111 22222222 + var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/; + // 11111111 22222222 + var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/; - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + var dataPerTracked = {}; - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + return { + restrict: 'A', + compile: function(tElement, tAttrs) { + var linkedScopes = []; + var instances = []; + var expToData = {}; + var lastActivated = null; + var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP); + var onExp = onExpMatches[2]; + var expsStr = onExpMatches[1]; + var exps = expsStr.split(','); + + return linkFn; + + function linkFn(scope, element, attrs) { + linkedScopes.push(scope); + instances.push({ + scope: scope, + element: element + }); - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; + exps.forEach(function(exp, k) { + addForExp(exp, scope); + }); - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + scope.$on('$destroy', removeScope); + } - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); + function addForExp(exp, scope) { + var matches = exp.match(IS_REGEXP); + var clazz = scope.$eval(matches[1]); + var compareWithExp = matches[2]; + var data = expToData[exp]; + if (!data) { + var watchFn = function(compareWithVal) { + var newActivated = null; + instances.some(function(instance) { + var thisVal = instance.scope.$eval(onExp); + if (thisVal === compareWithVal) { + newActivated = instance; + return true; + } + }); + if (data.lastActivated !== newActivated) { + if (data.lastActivated) { + $animate.removeClass(data.lastActivated.element, clazz); + } + if (newActivated) { + $animate.addClass(newActivated.element, clazz); + } + data.lastActivated = newActivated; + } + }; + expToData[exp] = data = { + lastActivated: null, + scope: scope, + watchFn: watchFn, + compareWithExp: compareWithExp, + watcher: scope.$watch(compareWithExp, watchFn) + }; + } + data.watchFn(scope.$eval(compareWithExp)); + } - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; + function removeScope(e) { + var removedScope = e.targetScope; + var index = linkedScopes.indexOf(removedScope); + linkedScopes.splice(index, 1); + instances.splice(index, 1); + if (linkedScopes.length) { + var newWatchScope = linkedScopes[0]; + angular.forEach(expToData, function(data) { + if (data.scope === removedScope) { + data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn); + data.scope = newWatchScope; + } + }); + } else { + expToData = {}; } - - return targetElPos; } - }; - }]); + } + }; +}]); +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass']) + +.value('$datepickerSuppressError', false) -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) +.value('$datepickerLiteralWarning', true) -.constant('datepickerConfig', { +.constant('uibDatepickerConfig', { + datepickerMode: 'day', formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', + maxDate: null, maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, minDate: null, - maxDate: null, - shortcutPropagation: false + minMode: 'day', + monthColumns: 3, + ngModelOptions: {}, + shortcutPropagation: false, + showWeeks: true, + yearColumns: 5, + yearRows: 4 }) -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { +.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', + function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; + ngModelOptions = {}, + watchListeners = []; + + $element.addClass('uib-datepicker'); + $attrs.$set('role', 'application'); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } // Modes chain this.modes = ['day', 'month', 'year']; - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); + [ + 'customClass', + 'dateDisabled', + 'datepickerMode', + 'formatDay', + 'formatDayHeader', + 'formatDayTitle', + 'formatMonth', + 'formatMonthTitle', + 'formatYear', + 'maxDate', + 'maxMode', + 'minDate', + 'minMode', + 'monthColumns', + 'showWeeks', + 'shortcutPropagation', + 'startingDay', + 'yearColumns', + 'yearRows' + ].forEach(function(key) { + switch (key) { + case 'customClass': + case 'dateDisabled': + $scope[key] = $scope.datepickerOptions[key] || angular.noop; + break; + case 'datepickerMode': + $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ? + $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode; + break; + case 'formatDay': + case 'formatDayHeader': + case 'formatDayTitle': + case 'formatMonth': + case 'formatMonthTitle': + case 'formatYear': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $interpolate($scope.datepickerOptions[key])($scope.$parent) : + datepickerConfig[key]; + break; + case 'monthColumns': + case 'showWeeks': + case 'shortcutPropagation': + case 'yearColumns': + case 'yearRows': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $scope.datepickerOptions[key] : datepickerConfig[key]; + break; + case 'startingDay': + if (angular.isDefined($scope.datepickerOptions.startingDay)) { + self.startingDay = $scope.datepickerOptions.startingDay; + } else if (angular.isNumber(datepickerConfig.startingDay)) { + self.startingDay = datepickerConfig.startingDay; + } else { + self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7; + } - // Watchable date attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + break; + case 'maxDate': + case 'minDate': + $scope.$watch('datepickerOptions.' + key, function(value) { + if (value) { + if (angular.isDate(value)) { + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone')); + } else { + if ($datepickerLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + self[key] = new Date(dateFilter(value, 'medium')); + } + } else { + self[key] = datepickerConfig[key] ? + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) : + null; + } + + self.refreshView(); + }); + + break; + case 'maxMode': + case 'minMode': + if ($scope.datepickerOptions[key]) { + $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) { + self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key]; + if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) || + key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) { + $scope.datepickerMode = self[key]; + $scope.datepickerOptions.datepickerMode = self[key]; + } + }); + } else { + self[key] = $scope[key] = datepickerConfig[key] || null; + } + + break; } }); - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; - $scope.maxMode = self.maxMode; $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - if(angular.isDefined($attrs.initDate)) { - this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date(); - $scope.$parent.$watch($attrs.initDate, function(initDate){ - if(initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)){ - self.activeDate = initDate; - self.refreshView(); - } - }); - } else { - this.activeDate = new Date(); + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if (angular.isDefined($attrs.ngDisabled)) { + watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) { + $scope.disabled = disabled; + self.refreshView(); + })); } $scope.isActive = function(dateObject) { @@ -1076,8 +1588,26 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return false; }; - this.init = function( ngModelCtrl_ ) { + this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; + ngModelOptions = extractOptions(ngModelCtrl); + + if ($scope.datepickerOptions.initDate) { + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); + $scope.$watch('datepickerOptions.initDate', function(initDate) { + if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); + self.refreshView(); + } + }); + } else { + self.activeDate = new Date(); + } + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); + this.activeDate = !isNaN(date) ? + dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : + dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); @@ -1085,48 +1615,72 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }; this.render = function() { - if ( ngModelCtrl.$viewValue ) { - var date = new Date( ngModelCtrl.$viewValue ), + if (ngModelCtrl.$viewValue) { + var date = new Date(ngModelCtrl.$viewValue), isValid = !isNaN(date); - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + if (isValid) { + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); + } else if (!$datepickerSuppressError) { + $log.error('Datepicker directive: "ng-model" value must be a Date object'); } - ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { - if ( this.element ) { + if (this.element) { + $scope.selectedDt = null; this._refreshView(); + if ($scope.activeDt) { + $scope.activeDateId = $scope.activeDt.uid; + } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); + ngModelCtrl.$setValidity('dateDisabled', !date || + this.element && !this.isDisabled(date)); } }; this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - return { + model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); + var today = new Date(); + today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); + var time = this.compare(date, today); + var dt = { date: date, - label: dateFilter(date, format), + label: dateParser.filter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0, - customClass: this.customClass(date) + past: time < 0, + current: time === 0, + future: time > 0, + customClass: this.customClass(date) || null }; + + if (model && this.compare(date, model) === 0) { + $scope.selectedDt = dt; + } + + if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) { + $scope.activeDt = dt; + } + + return dt; }; - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + this.isDisabled = function(date) { + return $scope.disabled || + this.minDate && this.compare(date, this.minDate) < 0 || + this.maxDate && this.compare(date, this.maxDate) > 0 || + $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}); }; - this.customClass = function( date ) { - return $scope.customClass({date: date, mode: $scope.datepickerMode}); - }; + this.customClass = function(date) { + return $scope.customClass({date: date, mode: $scope.datepickerMode}); + }; // Split array into smaller arrays this.split = function(arr, size) { @@ -1137,667 +1691,1702 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return arrays; }; - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); + $scope.select = function(date) { + if ($scope.datepickerMode === self.minMode) { + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); + ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]); + + $scope.$emit('uib:datepicker.mode'); } + + $scope.$broadcast('uib:datepicker.focus'); }; - $scope.move = function( direction ) { + $scope.move = function(direction) { var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), month = self.activeDate.getMonth() + direction * (self.step.months || 0); self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; - $scope.toggleMode = function( direction ) { + $scope.toggleMode = function(direction) { direction = direction || 1; - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + if ($scope.datepickerMode === self.maxMode && direction === 1 || + $scope.datepickerMode === self.minMode && direction === -1) { return; } - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]); + + $scope.$emit('uib:datepicker.mode'); }; // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); + self.element[0].focus(); }; // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); + $scope.$on('uib:datepicker.focus', focusElement); - $scope.keydown = function( evt ) { + $scope.keydown = function(evt) { var key = $scope.keys[evt.which]; - if ( !key || evt.shiftKey || evt.altKey ) { + if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { return; } evt.preventDefault(); - if(!self.shortcutPropagation){ - evt.stopPropagation(); + if (!self.shortcutPropagation) { + evt.stopPropagation(); } if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { + if (self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); - focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; -}]) -.directive( 'datepicker', function () { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&', - customClass: '&', - shortcutPropagation: '&?' - }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', - link: function(scope, element, attrs, ctrls) { - var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + $element.on('keydown', function(evt) { + $scope.$apply(function() { + $scope.keydown(evt); + }); + }); - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } + $scope.$on('$destroy', function() { + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); } - }; -}) + }); -.directive('daypicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; + function setMode(mode) { + $scope.datepickerMode = mode; + $scope.datepickerOptions.datepickerMode = mode; + } - ctrl.step = { months: 1 }; - ctrl.element = element; + function extractOptions(ngModelCtrl) { + var ngModelOptions; - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || + $scope.datepickerOptions.ngModelOptions || + datepickerConfig.ngModelOptions || + {}; - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value + var timezone = ngModelCtrl.$options.getOption('timezone') || + ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || + (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); + + // values passed to createChild override existing values + ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance + .createChild(datepickerConfig.ngModelOptions) // lowest precedence + .createChild($scope.datepickerOptions.ngModelOptions) + .createChild(ngModelCtrl.$options) // highest precedence + .createChild({timezone: timezone}); // to keep from squashing a non-null value + } - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); + return ngModelOptions; + } +}]) - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } +.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } + this.step = { months: 1 }; + this.element = $element; + function getDaysInMonth(year, month) { + return month === 1 && year % 4 === 0 && + (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month]; + } - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } + this.init = function(ctrl) { + angular.extend(ctrl, this); + scope.showWeeks = ctrl.showWeeks; + ctrl.refreshView(); + }; - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); + this.getDates = function(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0, date; + while (i < n) { + date = new Date(current); + dates[i++] = date; + current.setDate(current.getDate() + 1); + } + return dates; + }; - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7, - numWeeks = scope.rows.length; - for (var curWeek = 0; curWeek < numWeeks; curWeek++) { - scope.weekNumbers.push( - getISO8601WeekNumber( scope.rows[curWeek][thursdayIndex].date )); - } - } - }; + this._refreshView = function() { + var year = this.activeDate.getFullYear(), + month = this.activeDate.getMonth(), + firstDayOfMonth = new Date(this.activeDate); - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; + firstDayOfMonth.setFullYear(year, month, 1); + + var difference = this.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = difference > 0 ? + 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if (numDisplayedFromPreviousMonth > 0) { + firstDate.setDate(-numDisplayedFromPreviousMonth + 1); + } - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); + // 42 is the number of days on a six-week calendar + var days = this.getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, this.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') }; + } - ctrl.refreshView(); + scope.title = dateFilter(this.activeDate, this.formatDayTitle); + scope.rows = this.split(days, 7); + + if (scope.showWeeks) { + scope.weekNumbers = []; + var thursdayIndex = (4 + 7 - this.startingDay) % 7, + numWeeks = scope.rows.length; + for (var curWeek = 0; curWeek < numWeeks; curWeek++) { + scope.weekNumbers.push( + getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); + } + } + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 7; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()); } + this.activeDate.setDate(date); }; }]) -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } +.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + this.step = { years: 1 }; + this.element = $element; - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; + this.init = function(ctrl) { + angular.extend(ctrl, this); + ctrl.refreshView(); + }; - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; + this._refreshView = function() { + var months = new Array(12), + year = this.activeDate.getFullYear(), + date; - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; + for (var i = 0; i < 12; i++) { + date = new Date(this.activeDate); + date.setFullYear(year, i, 1); + months[i] = angular.extend(this.createDateObject(date, this.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } - ctrl.refreshView(); + scope.title = dateFilter(this.activeDate, this.formatMonthTitle); + scope.rows = this.split(months, this.monthColumns); + scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1; + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - this.monthColumns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + this.monthColumns; + } else if (key === 'pageup' || key === 'pagedown') { + var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; } + this.activeDate.setMonth(date); }; }]) -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; +.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var columns, range; + this.element = $element; - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } + function getStartingYear(year) { + return parseInt((year - 1) / range, 10) * range + 1; + } - ctrl._refreshView = function() { - var years = new Array(range); + this.yearpickerInit = function() { + columns = this.yearColumns; + range = this.yearRows * columns; + this.step = { years: range }; + }; - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } + this._refreshView = function() { + var years = new Array(range), date; - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; + for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) { + date = new Date(this.activeDate); + date.setFullYear(start + i, 0, 1); + years[i] = angular.extend(this.createDateObject(date, this.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = this.split(years, columns); + scope.columns = columns; + }; - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; + this.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; - ctrl.refreshView(); + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - columns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + columns; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * range; + } else if (key === 'home') { + date = getStartingYear(this.activeDate.getFullYear()); + } else if (key === 'end') { + date = getStartingYear(this.activeDate.getFullYear()) + range - 1; } + this.activeDate.setFullYear(date); }; }]) -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - html5Types: { - date: 'yyyy-MM-dd', - 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', - 'month': 'yyyy-MM' - }, - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { +.directive('uibDatepicker', function() { return { - restrict: 'EA', - require: 'ngModel', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; + }, scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&', - customClass: '&' + datepickerOptions: '=?' }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; - - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + require: ['uibDatepicker', '^ngModel'], + restrict: 'A', + controller: 'UibDatepickerController', + controllerAs: 'datepicker', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; + datepickerCtrl.init(ngModelCtrl); + } + }; +}) - var isHtml5DateInput = false; - if (datepickerPopupConfig.html5Types[attrs.type]) { - dateFormat = datepickerPopupConfig.html5Types[attrs.type]; - isHtml5DateInput = true; - } else { - dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; - attrs.$observe('datepickerPopup', function(value, oldValue) { - var newDateFormat = value || datepickerPopupConfig.datepickerPopup; - // Invalidate the $modelValue to ensure that formatters re-run - // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 - if (newDateFormat !== dateFormat) { - dateFormat = newDateFormat; - ngModel.$modelValue = null; - - if (!dateFormat) { - throw new Error('datepickerPopup must have a date format specified.'); - } - } - }); - } +.directive('uibDaypicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/day.html'; + }, + require: ['^uibDatepicker', 'uibDaypicker'], + restrict: 'A', + controller: 'UibDaypickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + daypickerCtrl = ctrls[1]; - if (!dateFormat) { - throw new Error('datepickerPopup must have a date format specified.'); - } + daypickerCtrl.init(datepickerCtrl); + } + }; +}) - if (isHtml5DateInput && attrs.datepickerPopup) { - throw new Error('HTML5 date input types do not support custom formats.'); - } +.directive('uibMonthpicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/month.html'; + }, + require: ['^uibDatepicker', 'uibMonthpicker'], + restrict: 'A', + controller: 'UibMonthpickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + monthpickerCtrl = ctrls[1]; - // popup element used to display calendar - var popupEl = angular.element('
        '); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); + monthpickerCtrl.init(datepickerCtrl); + } + }; +}) - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } +.directive('uibYearpicker', function() { + return { + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/year.html'; + }, + require: ['^uibDatepicker', 'uibYearpicker'], + restrict: 'A', + controller: 'UibYearpickerController', + link: function(scope, element, attrs, ctrls) { + var ctrl = ctrls[0]; + angular.extend(ctrl, ctrls[1]); + ctrl.yearpickerInit(); - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if (isHtml5DateInput) { - if (attrs.type == 'month') { - datepickerEl.attr('datepicker-mode', '"month"'); - datepickerEl.attr('min-mode', 'month'); - } - } + ctrl.refreshView(); + } + }; +}); - if ( attrs.datepickerOptions ) { - var options = scope.$parent.$eval(attrs.datepickerOptions); - if(options.initDate) { - scope.initDate = options.initDate; - datepickerEl.attr( 'init-date', 'initDate' ); - delete options.initDate; - } - angular.forEach(options, function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); - }); - } +angular.module('ui.bootstrap.position', []) - scope.watchData = {}; - angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) { - if ( attrs[key] ) { - var getAttribute = $parse(attrs[key]); - scope.$parent.$watch(getAttribute, function(value){ - scope.watchData[key] = value; - }); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if ( key === 'datepickerMode' ) { - var setAttribute = getAttribute.assign; - scope.$watch('watchData.' + key, function(value, oldvalue) { - if ( angular.isFunction(setAttribute) && value !== oldvalue ) { - setAttribute(scope.$parent, value); - } - }); - } - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } +/** + * A set of utility methods for working with the DOM. + * It is meant to be used where we need to absolute-position elements in + * relation to another element (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$uibPosition', ['$document', '$window', function($document, $window) { + /** + * Used by scrollbarWidth() function to cache scrollbar's width. + * Do not access this variable directly, use scrollbarWidth() instead. + */ + var SCROLLBAR_WIDTH; + /** + * scrollbar on body and html element in IE and Edge overlay + * content and should be considered 0 width. + */ + var BODY_SCROLLBAR_WIDTH; + var OVERFLOW_REGEX = { + normal: /(auto|scroll)/, + hidden: /(auto|scroll|hidden)/ + }; + var PLACEMENT_REGEX = { + auto: /\s?auto?\s?/i, + primary: /^(top|bottom|left|right)$/, + secondary: /^(top|bottom|left|right|center)$/, + vertical: /^(top|bottom)$/ + }; + var BODY_REGEX = /(HTML|BODY)/; - if (attrs.showWeeks) { - datepickerEl.attr('show-weeks', attrs.showWeeks); - } + return { - if (attrs.customClass){ - datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); - } + /** + * Provides a raw DOM element from a jQuery/jQLite element. + * + * @param {element} elem - The element to convert. + * + * @returns {element} A HTML element. + */ + getRawNode: function(elem) { + return elem.nodeName ? elem : elem[0] || elem; + }, - function parseDate(viewValue) { - if (angular.isNumber(viewValue)) { - // presumably timestamp to date object - viewValue = new Date(viewValue); - } + /** + * Provides a parsed number for a style property. Strips + * units and casts invalid numbers to 0. + * + * @param {string} value - The style value to parse. + * + * @returns {number} A valid number. + */ + parseStyle: function(value) { + value = parseFloat(value); + return isFinite(value) ? value : 0; + }, - if (!viewValue) { - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue); - if (isNaN(date)) { - return undefined; - } else { - return date; + /** + * Provides the closest positioned ancestor. + * + * @param {element} element - The element to get the offest parent for. + * + * @returns {element} The closest positioned ancestor. + */ + offsetParent: function(elem) { + elem = this.getRawNode(elem); + + var offsetParent = elem.offsetParent || $document[0].documentElement; + + function isStaticPositioned(el) { + return ($window.getComputedStyle(el).position || 'static') === 'static'; + } + + while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + + return offsetParent || $document[0].documentElement; + }, + + /** + * Provides the scrollbar width, concept from TWBS measureScrollbar() + * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js + * In IE and Edge, scollbar on body and html element overlay and should + * return a width of 0. + * + * @returns {number} The width of the browser scollbar. + */ + scrollbarWidth: function(isBody) { + if (isBody) { + if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { + var bodyElem = $document.find('body'); + bodyElem.addClass('uib-position-body-scrollbar-measure'); + BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; + BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; + bodyElem.removeClass('uib-position-body-scrollbar-measure'); } + return BODY_SCROLLBAR_WIDTH; + } + + if (angular.isUndefined(SCROLLBAR_WIDTH)) { + var scrollElem = angular.element('
        '); + $document.find('body').append(scrollElem); + SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; + SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; + scrollElem.remove(); + } + + return SCROLLBAR_WIDTH; + }, + + /** + * Provides the padding required on an element to replace the scrollbar. + * + * @returns {object} An object with the following properties: + *
          + *
        • **scrollbarWidth**: the width of the scrollbar
        • + *
        • **widthOverflow**: whether the the width is overflowing
        • + *
        • **right**: the amount of right padding on the element needed to replace the scrollbar
        • + *
        • **rightOriginal**: the amount of right padding currently on the element
        • + *
        • **heightOverflow**: whether the the height is overflowing
        • + *
        • **bottom**: the amount of bottom padding on the element needed to replace the scrollbar
        • + *
        • **bottomOriginal**: the amount of bottom padding currently on the element
        • + *
        + */ + scrollbarPadding: function(elem) { + elem = this.getRawNode(elem); + + var elemStyle = $window.getComputedStyle(elem); + var paddingRight = this.parseStyle(elemStyle.paddingRight); + var paddingBottom = this.parseStyle(elemStyle.paddingBottom); + var scrollParent = this.scrollParent(elem, false, true); + var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName)); + + return { + scrollbarWidth: scrollbarWidth, + widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, + right: paddingRight + scrollbarWidth, + originalRight: paddingRight, + heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, + bottom: paddingBottom + scrollbarWidth, + originalBottom: paddingBottom + }; + }, + + /** + * Checks to see if the element is scrollable. + * + * @param {element} elem - The element to check. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * + * @returns {boolean} Whether the element is scrollable. + */ + isScrollable: function(elem, includeHidden) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var elemStyle = $window.getComputedStyle(elem); + return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); + }, + + /** + * Provides the closest scrollable ancestor. + * A port of the jQuery UI scrollParent method: + * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js + * + * @param {element} elem - The element to find the scroll parent of. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * @param {boolean=} [includeSelf=false] - Should the element being passed be + * included in the scrollable llokup. + * + * @returns {element} A HTML element. + */ + scrollParent: function(elem, includeHidden, includeSelf) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var documentEl = $document[0].documentElement; + var elemStyle = $window.getComputedStyle(elem); + if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { + return elem; + } + var excludeStatic = elemStyle.position === 'absolute'; + var scrollParent = elem.parentElement || documentEl; + + if (scrollParent === documentEl || elemStyle.position === 'fixed') { + return documentEl; + } + + while (scrollParent.parentElement && scrollParent !== documentEl) { + var spStyle = $window.getComputedStyle(scrollParent); + if (excludeStatic && spStyle.position !== 'static') { + excludeStatic = false; + } + + if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { + break; + } + scrollParent = scrollParent.parentElement; + } + + return scrollParent; + }, + + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ - distance to closest positioned + * ancestor. Does not account for margins by default like jQuery position. + * + * @param {element} elem - The element to caclulate the position on. + * @param {boolean=} [includeMargins=false] - Should margins be accounted + * for, default is false. + * + * @returns {object} An object with the following properties: + *
          + *
        • **width**: the width of the element
        • + *
        • **height**: the height of the element
        • + *
        • **top**: distance to top edge of offset parent
        • + *
        • **left**: distance to left edge of offset parent
        • + *
        + */ + position: function(elem, includeMagins) { + elem = this.getRawNode(elem); + + var elemOffset = this.offset(elem); + if (includeMagins) { + var elemStyle = $window.getComputedStyle(elem); + elemOffset.top -= this.parseStyle(elemStyle.marginTop); + elemOffset.left -= this.parseStyle(elemStyle.marginLeft); + } + var parent = this.offsetParent(elem); + var parentOffset = {top: 0, left: 0}; + + if (parent !== $document[0].documentElement) { + parentOffset = this.offset(parent); + parentOffset.top += parent.clientTop - parent.scrollTop; + parentOffset.left += parent.clientLeft - parent.scrollLeft; + } + + return { + width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), + top: Math.round(elemOffset.top - parentOffset.top), + left: Math.round(elemOffset.left - parentOffset.left) + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ - distance to viewport. Does + * not account for borders, margins, or padding on the body + * element. + * + * @param {element} elem - The element to calculate the offset on. + * + * @returns {object} An object with the following properties: + *
          + *
        • **width**: the width of the element
        • + *
        • **height**: the height of the element
        • + *
        • **top**: distance to top edge of viewport
        • + *
        • **right**: distance to bottom edge of viewport
        • + *
        + */ + offset: function(elem) { + elem = this.getRawNode(elem); + + var elemBCR = elem.getBoundingClientRect(); + return { + width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), + top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), + left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) + }; + }, + + /** + * Provides offset distance to the closest scrollable ancestor + * or viewport. Accounts for border and scrollbar width. + * + * Right and bottom dimensions represent the distance to the + * respective edge of the viewport element. If the element + * edge extends beyond the viewport, a negative value will be + * reported. + * + * @param {element} elem - The element to get the viewport offset for. + * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead + * of the first scrollable element, default is false. + * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element + * be accounted for, default is true. + * + * @returns {object} An object with the following properties: + *
          + *
        • **top**: distance to the top content edge of viewport element
        • + *
        • **bottom**: distance to the bottom content edge of viewport element
        • + *
        • **left**: distance to the left content edge of viewport element
        • + *
        • **right**: distance to the right content edge of viewport element
        • + *
        + */ + viewportOffset: function(elem, useDocument, includePadding) { + elem = this.getRawNode(elem); + includePadding = includePadding !== false ? true : false; + + var elemBCR = elem.getBoundingClientRect(); + var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; + + var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); + var offsetParentBCR = offsetParent.getBoundingClientRect(); + + offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; + offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; + if (offsetParent === $document[0].documentElement) { + offsetBCR.top += $window.pageYOffset; + offsetBCR.left += $window.pageXOffset; + } + offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; + offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; + + if (includePadding) { + var offsetParentStyle = $window.getComputedStyle(offsetParent); + offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); + offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); + offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); + offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); + } + + return { + top: Math.round(elemBCR.top - offsetBCR.top), + bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), + left: Math.round(elemBCR.left - offsetBCR.left), + right: Math.round(offsetBCR.right - elemBCR.right) + }; + }, + + /** + * Provides an array of placement values parsed from a placement string. + * Along with the 'auto' indicator, supported placement strings are: + *
          + *
        • top: element on top, horizontally centered on host element.
        • + *
        • top-left: element on top, left edge aligned with host element left edge.
        • + *
        • top-right: element on top, lerightft edge aligned with host element right edge.
        • + *
        • bottom: element on bottom, horizontally centered on host element.
        • + *
        • bottom-left: element on bottom, left edge aligned with host element left edge.
        • + *
        • bottom-right: element on bottom, right edge aligned with host element right edge.
        • + *
        • left: element on left, vertically centered on host element.
        • + *
        • left-top: element on left, top edge aligned with host element top edge.
        • + *
        • left-bottom: element on left, bottom edge aligned with host element bottom edge.
        • + *
        • right: element on right, vertically centered on host element.
        • + *
        • right-top: element on right, top edge aligned with host element top edge.
        • + *
        • right-bottom: element on right, bottom edge aligned with host element bottom edge.
        • + *
        + * A placement string with an 'auto' indicator is expected to be + * space separated from the placement, i.e: 'auto bottom-left' If + * the primary and secondary placement values do not match 'top, + * bottom, left, right' then 'top' will be the primary placement and + * 'center' will be the secondary placement. If 'auto' is passed, true + * will be returned as the 3rd value of the array. + * + * @param {string} placement - The placement string to parse. + * + * @returns {array} An array with the following values + *
          + *
        • **[0]**: The primary placement.
        • + *
        • **[1]**: The secondary placement.
        • + *
        • **[2]**: If auto is passed: true, else undefined.
        • + *
        + */ + parsePlacement: function(placement) { + var autoPlace = PLACEMENT_REGEX.auto.test(placement); + if (autoPlace) { + placement = placement.replace(PLACEMENT_REGEX.auto, ''); + } + + placement = placement.split('-'); + + placement[0] = placement[0] || 'top'; + if (!PLACEMENT_REGEX.primary.test(placement[0])) { + placement[0] = 'top'; + } + + placement[1] = placement[1] || 'center'; + if (!PLACEMENT_REGEX.secondary.test(placement[1])) { + placement[1] = 'center'; + } + + if (autoPlace) { + placement[2] = true; } else { - return undefined; + placement[2] = false; } - } - function validator(modelValue, viewValue) { - var value = modelValue || viewValue; - if (angular.isNumber(value)) { - value = new Date(value); + return placement; + }, + + /** + * Provides coordinates for an element to be positioned relative to + * another element. Passing 'auto' as part of the placement parameter + * will enable smart placement - where the element fits. i.e: + * 'auto left-top' will check to see if there is enough space to the left + * of the hostElem to fit the targetElem, if not place right (same for secondary + * top placement). Available space is calculated using the viewportOffset + * function. + * + * @param {element} hostElem - The element to position against. + * @param {element} targetElem - The element to position. + * @param {string=} [placement=top] - The placement for the targetElem, + * default is 'top'. 'center' is assumed as secondary placement for + * 'top', 'left', 'right', and 'bottom' placements. Available placements are: + *
          + *
        • top
        • + *
        • top-right
        • + *
        • top-left
        • + *
        • bottom
        • + *
        • bottom-left
        • + *
        • bottom-right
        • + *
        • left
        • + *
        • left-top
        • + *
        • left-bottom
        • + *
        • right
        • + *
        • right-top
        • + *
        • right-bottom
        • + *
        + * @param {boolean=} [appendToBody=false] - Should the top and left values returned + * be calculated from the body element, default is false. + * + * @returns {object} An object with the following properties: + *
          + *
        • **top**: Value for targetElem top.
        • + *
        • **left**: Value for targetElem left.
        • + *
        • **placement**: The resolved placement.
        • + *
        + */ + positionElements: function(hostElem, targetElem, placement, appendToBody) { + hostElem = this.getRawNode(hostElem); + targetElem = this.getRawNode(targetElem); + + // need to read from prop to support tests. + var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); + var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); + + placement = this.parsePlacement(placement); + + var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); + var targetElemPos = {top: 0, left: 0, placement: ''}; + + if (placement[2]) { + var viewportOffset = this.viewportOffset(hostElem, appendToBody); + + var targetElemStyle = $window.getComputedStyle(targetElem); + var adjustedSize = { + width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), + height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) + }; + + placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : + placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : + placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : + placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : + placement[0]; + + placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : + placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : + placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : + placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : + placement[1]; + + if (placement[1] === 'center') { + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + var xOverflow = hostElemPos.width / 2 - targetWidth / 2; + if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { + placement[1] = 'left'; + } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { + placement[1] = 'right'; + } + } else { + var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; + if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { + placement[1] = 'top'; + } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { + placement[1] = 'bottom'; + } + } + } } - if (!value) { - return true; - } else if (angular.isDate(value) && !isNaN(value)) { - return true; - } else if (angular.isString(value)) { - var date = dateParser.parse(value, dateFormat) || new Date(value); - return !isNaN(date); + + switch (placement[0]) { + case 'top': + targetElemPos.top = hostElemPos.top - targetHeight; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height; + break; + case 'left': + targetElemPos.left = hostElemPos.left - targetWidth; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width; + break; + } + + switch (placement[1]) { + case 'top': + targetElemPos.top = hostElemPos.top; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; + break; + case 'left': + targetElemPos.left = hostElemPos.left; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; + break; + case 'center': + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; + } else { + targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; + } + break; + } + + targetElemPos.top = Math.round(targetElemPos.top); + targetElemPos.left = Math.round(targetElemPos.left); + targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; + + return targetElemPos; + }, + + /** + * Provides a way to adjust the top positioning after first + * render to correctly align element to top after content + * rendering causes resized element height + * + * @param {array} placementClasses - The array of strings of classes + * element should have. + * @param {object} containerPosition - The object with container + * position information + * @param {number} initialHeight - The initial height for the elem. + * @param {number} currentHeight - The current height for the elem. + */ + adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) { + if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) { + return { + top: containerPosition.top - currentHeight + 'px' + }; + } + }, + + /** + * Provides a way for positioning tooltip & dropdown + * arrows when using placement options beyond the standard + * left, right, top, or bottom. + * + * @param {element} elem - The tooltip/dropdown element. + * @param {string} placement - The placement for the elem. + */ + positionArrow: function(elem, placement) { + elem = this.getRawNode(elem); + + var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); + if (!innerElem) { + return; + } + + var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); + + var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); + if (!arrowElem) { + return; + } + + var arrowCss = { + top: '', + bottom: '', + left: '', + right: '' + }; + + placement = this.parsePlacement(placement); + if (placement[1] === 'center') { + // no adjustment necessary - just reset styles + angular.element(arrowElem).css(arrowCss); + return; + } + + var borderProp = 'border-' + placement[0] + '-width'; + var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; + + var borderRadiusProp = 'border-'; + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + borderRadiusProp += placement[0] + '-' + placement[1]; } else { - return false; + borderRadiusProp += placement[1] + '-' + placement[0]; } - } + borderRadiusProp += '-radius'; + var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; - if (!isHtml5DateInput) { - // Internal API to maintain the correct ng-invalid-[key] class - ngModel.$$parserName = 'date'; - ngModel.$validators.date = validator; - ngModel.$parsers.unshift(parseDate); - ngModel.$formatters.push(function (value) { - scope.date = value; - return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); - }); + switch (placement[0]) { + case 'top': + arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; + break; + case 'bottom': + arrowCss.top = isTooltip ? '0' : '-' + borderWidth; + break; + case 'left': + arrowCss.right = isTooltip ? '0' : '-' + borderWidth; + break; + case 'right': + arrowCss.left = isTooltip ? '0' : '-' + borderWidth; + break; + } + + arrowCss[placement[1]] = borderRadius; + + angular.element(arrowElem).css(arrowCss); } - else { - ngModel.$formatters.push(function (value) { - scope.date = value; - return value; - }); + }; + }]); + +angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) + +.value('$datepickerPopupLiteralWarning', true) + +.constant('uibDatepickerPopupConfig', { + altInputFormats: [], + appendToBody: false, + clearText: 'Clear', + closeOnDateSelection: true, + closeText: 'Done', + currentText: 'Today', + datepickerPopup: 'yyyy-MM-dd', + datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html', + datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html', + html5Types: { + date: 'yyyy-MM-dd', + 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', + 'month': 'yyyy-MM' + }, + onOpenFocus: true, + showButtonBar: true, + placement: 'auto bottom-left' +}) + +.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning', +function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = extractOptions(ngModel); + closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? + $scope.$parent.$eval($attrs.closeOnDateSelection) : + datepickerPopupConfig.closeOnDateSelection; + appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ? + $scope.$parent.$eval($attrs.datepickerAppendToBody) : + datepickerPopupConfig.appendToBody; + onOpenFocus = angular.isDefined($attrs.onOpenFocus) ? + $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus; + datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ? + $attrs.datepickerPopupTemplateUrl : + datepickerPopupConfig.datepickerPopupTemplateUrl; + datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ? + $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl; + altInputFormats = angular.isDefined($attrs.altInputFormats) ? + $scope.$parent.$eval($attrs.altInputFormats) : + datepickerPopupConfig.altInputFormats; + + $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ? + $scope.$parent.$eval($attrs.showButtonBar) : + datepickerPopupConfig.showButtonBar; + + if (datepickerPopupConfig.html5Types[$attrs.type]) { + dateFormat = datepickerPopupConfig.html5Types[$attrs.type]; + isHtml5DateInput = true; + } else { + dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; + $attrs.$observe('uibDatepickerPopup', function(value, oldValue) { + var newDateFormat = value || datepickerPopupConfig.datepickerPopup; + // Invalidate the $modelValue to ensure that formatters re-run + // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 + if (newDateFormat !== dateFormat) { + dateFormat = newDateFormat; + ngModel.$modelValue = null; + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + } + }); + } + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + + if (isHtml5DateInput && $attrs.uibDatepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } + + // popup element used to display calendar + popupEl = angular.element('
        '); + + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); + + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } + + if (isHtml5DateInput) { + if ($attrs.type === 'month') { + $scope.datepickerOptions.datepickerMode = 'month'; + $scope.datepickerOptions.minMode = 'month'; } + } - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; + datepickerEl.attr('datepicker-options', 'datepickerOptions'); + + if (!isHtml5DateInput) { + // Internal API to maintain the correct ng-invalid-[key] class + ngModel.$$parserName = 'date'; + ngModel.$validators.date = validator; + ngModel.$parsers.unshift(parseDate); + ngModel.$formatters.push(function(value) { + if (ngModel.$isEmpty(value)) { + $scope.date = value; + return value; } - var date = scope.date ? dateFilter(scope.date, dateFormat) : ''; - element.val(date); - ngModel.$setViewValue(date); - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); + if (angular.isNumber(value)) { + value = new Date(value); } - }; - // Detect changes in the view from the text box - ngModel.$viewChangeListeners.push(function () { - scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue); + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); + + return dateParser.filter($scope.date, dateFormat); }); + } else { + ngModel.$formatters.push(function(value) { + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); + return value; + }); + } - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + $scope.date = parseDateString(ngModel.$viewValue); + }); + + $element.on('keydown', inputKeydownBind); + + $popup = $compile(popupEl)($scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if (appendToBody) { + $document.find('body').append($popup); + } else { + $element.after($popup); + } + + $scope.$on('$destroy', function() { + if ($scope.isOpen === true) { + if (!$rootScope.$$phase) { + $scope.$apply(function() { + $scope.isOpen = false; }); } - }; + } - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); + $popup.remove(); + $element.off('keydown', inputKeydownBind); + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - if (scope.isOpen) { - evt.stopPropagation(); - } - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + }; - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); + $scope.getText = function(key) { + return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); + $scope.isDisabled = function(date) { + if (date === 'today') { + date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); + } + + var dates = {}; + angular.forEach(['minDate', 'maxDate'], function(key) { + if (!$scope.datepickerOptions[key]) { + dates[key] = null; + } else if (angular.isDate($scope.datepickerOptions[key])) { + dates[key] = new Date($scope.datepickerOptions[key]); + } else { + if ($datepickerPopupLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); } - }); - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(scope.date)) { - date = new Date(scope.date); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium')); + } + }); + + return $scope.datepickerOptions && + dates.minDate && $scope.compare(date, dates.minDate) < 0 || + dates.maxDate && $scope.compare(date, dates.maxDate) > 0; + }; + + $scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; + + // Inner change + $scope.dateSelection = function(dt) { + $scope.date = dt; + var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + $element.val(date); + ngModel.$setViewValue(date); + + if (closeOnDateSelection) { + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.keydown = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.select = function(date, evt) { + evt.stopPropagation(); + + if (date === 'today') { + var today = new Date(); + if (angular.isDate($scope.date)) { + date = new Date($scope.date); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); + date.setHours(0, 0, 0, 0); + } + } + $scope.dateSelection(date); + }; + + $scope.close = function(evt) { + evt.stopPropagation(); + + $scope.isOpen = false; + $element[0].focus(); + }; + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if ($attrs.ngDisabled) { + watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) { + $scope.disabled = disabled; + })); + } + + $scope.$watch('isOpen', function(value) { + if (value) { + if (!$scope.disabled) { + $timeout(function() { + positionPopup(); + + if (onOpenFocus) { + $scope.$broadcast('uib:datepicker.focus'); + } + + $document.on('click', documentClickBind); + + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + if (appendToBody || $position.parsePlacement(placement)[2]) { + scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element)); + if (scrollParentEl) { + scrollParentEl.on('scroll', positionPopup); + } } else { - date = new Date(today.setHours(0, 0, 0, 0)); + scrollParentEl = null; } + + angular.element($window).on('resize', positionPopup); + }, 0, false); + } else { + $scope.isOpen = false; + } + } else { + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + } + }); + + function cameltoDash(string) { + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + function parseDateString(viewValue) { + var date = dateParser.parse(viewValue, dateFormat, $scope.date); + if (isNaN(date)) { + for (var i = 0; i < altInputFormats.length; i++) { + date = dateParser.parse(viewValue, altInputFormats[i], $scope.date); + if (!isNaN(date)) { + return date; } - scope.dateSelection( date ); - }; + } + } + return date; + } - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } - var $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); + if (!viewValue) { + return null; + } - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } + + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } + } - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); + return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; + } + + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; + + if (!$attrs.ngRequired && !value) { + return true; + } + + if (angular.isNumber(value)) { + value = new Date(value); + } + + if (!value) { + return true; + } + + if (angular.isDate(value) && !isNaN(value)) { + return true; + } + + if (angular.isString(value)) { + return !isNaN(parseDateString(value)); + } + + return false; + } + + function documentClickBind(event) { + if (!$scope.isOpen && $scope.disabled) { + return; + } + + var popup = $popup[0]; + var dpContainsTarget = $element[0].contains(event.target); + // The popup node may not be an element node + // In some browsers (IE) only element nodes have the 'contains' function + var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); + if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { + $scope.$apply(function() { + $scope.isOpen = false; }); } - }; + } + + function inputKeydownBind(evt) { + if (evt.which === 27 && $scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = false; + }); + $element[0].focus(); + } else if (evt.which === 40 && !$scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = true; + }); + } + } + + function positionPopup() { + if ($scope.isOpen) { + var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup')); + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + var position = $position.positionElements($element, dpElement, placement, appendToBody); + dpElement.css({top: position.top + 'px', left: position.left + 'px'}); + if (dpElement.hasClass('uib-position-measure')) { + dpElement.removeClass('uib-position-measure'); + } + } + } + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = angular.isObject(ngModelCtrl.$options) ? + ngModelCtrl.$options : + { + timezone: null + }; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } + + $scope.$on('uib:datepicker.mode', function() { + $timeout(positionPopup, 0, false); + }); }]) -.directive('datepickerPopupWrap', function() { +.directive('uibDatepickerPopup', function() { return { - restrict:'EA', - replace: true, + require: ['ngModel', 'uibDatepickerPopup'], + controller: 'UibDatepickerPopupController', + scope: { + datepickerOptions: '=?', + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@' + }, + link: function(scope, element, attrs, ctrls) { + var ngModel = ctrls[0], + ctrl = ctrls[1]; + + ctrl.init(ngModel); + } + }; +}) + +.directive('uibDatepickerPopupWrap', function() { + return { + restrict: 'A', transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; } }; }); -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.debounce', []) +/** + * A helper, internal service that debounces a function + */ + .factory('$$debounce', ['$timeout', function($timeout) { + return function(callback, debounceTime) { + var timeoutPromise; + + return function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + + timeoutPromise = $timeout(function() { + callback.apply(self, args); + }, debounceTime); + }; + }; + }]); + +angular.module('ui.bootstrap.multiMap', []) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }); + +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) -.constant('dropdownConfig', { +.constant('uibDropdownConfig', { + appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) { +.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) { var openScope = null; + var openedContainers = $$multiMap.createNew(); + + this.isOnlyOpen = function(dropdownScope, appendTo) { + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (openDropdown) { + return openedDropdowns.length === 1; + } + } + + return false; + }; - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', escapeKeyBind); + this.open = function(dropdownScope, element, appendTo) { + if (!openScope) { + $document.on('click', closeDropdown); } - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; + if (openScope && openScope !== dropdownScope) { + openScope.isOpen = false; } openScope = dropdownScope; + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openedScopes = openedDropdowns.map(function(dropdown) { + return dropdown.scope; + }); + if (openedScopes.indexOf(dropdownScope) === -1) { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } + } else { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } }; - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { + this.close = function(dropdownScope, element, appendTo) { + if (openScope === dropdownScope) { + $document.off('click', closeDropdown); + $document.off('keydown', this.keybindFilter); openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', escapeKeyBind); + } + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); + } } }; - var closeDropdown = function( evt ) { + var closeDropdown = function(evt) { // This method may still be called during the same mouse event that // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } + if (!openScope || !openScope.isOpen) { return; } + + if (evt && openScope.getAutoClose() === 'disabled') { return; } - if( evt && openScope.getAutoClose() === 'disabled' ) { return ; } + if (evt && evt.which === 3) { return; } var toggleElement = openScope.getToggleElement(); - if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { - return; + if (evt && toggleElement && toggleElement[0].contains(evt.target)) { + return; } - var $element = openScope.getElement(); - if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) { + var dropdownElement = openScope.getDropdownElement(); + if (evt && openScope.getAutoClose() === 'outsideClick' && + dropdownElement && dropdownElement[0].contains(evt.target)) { return; } + openScope.focusToggleElement(); openScope.isOpen = false; if (!$rootScope.$$phase) { @@ -1805,28 +3394,45 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; - var escapeKeyBind = function( evt ) { - if ( evt.which === 27 ) { + this.keybindFilter = function(evt) { + if (!openScope) { + // see this.close as ESC could have been pressed which kills the scope so we can not proceed + return; + } + + var dropdownElement = openScope.getDropdownElement(); + var toggleElement = openScope.getToggleElement(); + var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target); + var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target); + if (evt.which === 27) { + evt.stopPropagation(); openScope.focusToggleElement(); closeDropdown(); + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) { + evt.preventDefault(); + evt.stopPropagation(); + openScope.focusDropdownEntry(evt.which); } }; }]) -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) { +.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - templateScope, - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, - appendToBody = false; - - this.init = function( element ) { - self.$element = element; - - if ( $attrs.isOpen ) { + scope = $scope.$new(), // create a child scope so we are not polluting original one + templateScope, + appendToOpenClass = dropdownConfig.appendToOpenClass, + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + keynavEnabled = false, + selectedOption = null, + body = $document.find('body'); + + $element.addClass('dropdown'); + + this.init = function() { + if ($attrs.isOpen) { getIsOpen = $parse($attrs.isOpen); setIsOpen = getIsOpen.assign; @@ -1835,18 +3441,16 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); } - appendToBody = angular.isDefined($attrs.dropdownAppendToBody); + keynavEnabled = angular.isDefined($attrs.keyboardNav); + }; - if ( appendToBody && self.dropdownMenu ) { - $document.find('body').append( self.dropdownMenu ); - element.on('$destroy', function handleDestroyEvent() { - self.dropdownMenu.remove(); - }); + this.toggle = function(open) { + scope.isOpen = arguments.length ? !!open : !scope.isOpen; + if (angular.isFunction(setIsOpen)) { + setIsOpen(scope, scope.isOpen); } - }; - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + return scope.isOpen; }; // Allow other directives to watch status @@ -1863,28 +3467,154 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }; scope.getElement = function() { - return self.$element; + return $element; + }; + + scope.isKeynavEnabled = function() { + return keynavEnabled; + }; + + scope.focusDropdownEntry = function(keyCode) { + var elems = self.dropdownMenu ? //If append to body is used. + angular.element(self.dropdownMenu).find('a') : + $element.find('ul').eq(0).find('a'); + + switch (keyCode) { + case 40: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = 0; + } else { + self.selectedOption = self.selectedOption === elems.length - 1 ? + self.selectedOption : + self.selectedOption + 1; + } + break; + } + case 38: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = elems.length - 1; + } else { + self.selectedOption = self.selectedOption === 0 ? + 0 : self.selectedOption - 1; + } + break; + } + } + elems[self.selectedOption].focus(); + }; + + scope.getDropdownElement = function() { + return self.dropdownMenu; }; scope.focusToggleElement = function() { - if ( self.toggleElement ) { + if (self.toggleElement) { self.toggleElement[0].focus(); } }; - scope.$watch('isOpen', function( isOpen, wasOpen ) { - if ( appendToBody && self.dropdownMenu ) { - var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true); - self.dropdownMenu.css({ + function removeDropdownMenu() { + $element.append(self.dropdownMenu); + } + + scope.$watch('isOpen', function(isOpen, wasOpen) { + var appendTo = null, + appendToBody = false; + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + if (angular.isDefined($attrs.dropdownAppendToBody)) { + var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope); + if (appendToBodyValue !== false) { + appendToBody = true; + } + } + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + if (isOpen) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', removeDropdownMenu); + } else { + $element.off('$destroy', removeDropdownMenu); + removeDropdownMenu(); + } + } + + if (appendTo && self.dropdownMenu) { + var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), + css, + rightalign, + scrollbarPadding, + scrollbarWidth = 0; + + css = { top: pos.top + 'px', - left: pos.left + 'px', display: isOpen ? 'block' : 'none' - }); + }; + + rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); + if (!rightalign) { + css.left = pos.left + 'px'; + css.right = 'auto'; + } else { + css.left = 'auto'; + scrollbarPadding = $position.scrollbarPadding(appendTo); + + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + scrollbarWidth = scrollbarPadding.scrollbarWidth; + } + + css.right = window.innerWidth - scrollbarWidth - + (pos.left + $element.prop('offsetWidth')) + 'px'; + } + + // Need to adjust our positioning to be relative to the appendTo container + // if it's not the body element + if (!appendToBody) { + var appendOffset = $position.offset(appendTo); + + css.top = pos.top - appendOffset.top + 'px'; + + if (!rightalign) { + css.left = pos.left - appendOffset.left + 'px'; + } else { + css.right = window.innerWidth - + (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; + } + } + + self.dropdownMenu.css(css); } - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + var openContainer = appendTo ? appendTo : $element; + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); + + if (hasOpenClass === !isOpen) { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + } - if ( isOpen ) { + if (isOpen) { if (self.dropdownMenuTemplateUrl) { $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { templateScope = scope.$new(); @@ -1892,13 +3622,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) var newEl = dropdownElement; self.dropdownMenu.replaceWith(newEl); self.dropdownMenu = newEl; + $document.on('keydown', uibDropdownService.keybindFilter); }); }); + } else { + $document.on('keydown', uibDropdownService.keybindFilter); } scope.focusToggleElement(); - dropdownService.open( scope ); + uibDropdownService.open(scope, $element, appendTo); } else { + uibDropdownService.close(scope, $element, appendTo); if (self.dropdownMenuTemplateUrl) { if (templateScope) { templateScope.$destroy(); @@ -1908,47 +3642,40 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) self.dropdownMenu = newEl; } - dropdownService.close( scope ); + self.selectedOption = null; } - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); + if (angular.isFunction(setIsOpen)) { + setIsOpen($scope, isOpen); } }); - - $scope.$on('$locationChangeSuccess', function() { - if (scope.getAutoClose() !== 'disabled') { - scope.isOpen = false; - } - }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); }]) -.directive('dropdown', function() { +.directive('uibDropdown', function() { return { - controller: 'DropdownController', + controller: 'UibDropdownController', link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); + dropdownCtrl.init(); } }; }) -.directive('dropdownMenu', function() { +.directive('uibDropdownMenu', function() { return { - restrict: 'AC', - require: '?^dropdown', + restrict: 'A', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if (!dropdownCtrl) { + if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { return; } + + element.addClass('dropdown-menu'); + var tplUrl = attrs.templateUrl; if (tplUrl) { dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; } + if (!dropdownCtrl.dropdownMenu) { dropdownCtrl.dropdownMenu = element; } @@ -1956,62 +3683,63 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }; }) -.directive('dropdownToggle', function() { +.directive('uibDropdownToggle', function() { return { - require: '?^dropdown', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { + if (!dropdownCtrl) { return; } + element.addClass('dropdown-toggle'); + dropdownCtrl.toggleElement = element; var toggleDropdown = function(event) { event.preventDefault(); - if ( !element.hasClass('disabled') && !attrs.disabled ) { + if (!element.hasClass('disabled') && !attrs.disabled) { scope.$apply(function() { dropdownCtrl.toggle(); }); } }; - element.bind('click', toggleDropdown); + element.on('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + scope.$watch(dropdownCtrl.isOpen, function(isOpen) { element.attr('aria-expanded', !!isOpen); }); scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); + element.off('click', toggleDropdown); }); } }; }); -angular.module('ui.bootstrap.modal', []) - +angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ - .factory('$$stackedMap', function () { + .factory('$$stackedMap', function() { return { - createNew: function () { + createNew: function() { var stack = []; return { - add: function (key, value) { + add: function(key, value) { stack.push({ key: key, value: value }); }, - get: function (key) { + get: function(key) { for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { return stack[i]; } } @@ -2023,153 +3751,252 @@ angular.module('ui.bootstrap.modal', []) } return keys; }, - top: function () { + top: function() { return stack[stack.length - 1]; }, - remove: function (key) { + remove: function(key) { var idx = -1; for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, - removeTop: function () { - return stack.splice(stack.length - 1, 1)[0]; + removeTop: function() { + return stack.pop(); }, - length: function () { + length: function() { return stack.length; } }; } }; + }); +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) +/** + * Pluggable resolve mechanism for the modal resolve resolution + * Supports UI Router's $resolve service + */ + .provider('$uibResolve', function() { + var resolve = this; + this.resolver = null; + + this.setResolver = function(resolver) { + this.resolver = resolver; + }; + + this.$get = ['$injector', '$q', function($injector, $q) { + var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; + return { + resolve: function(invocables, locals, parent, self) { + if (resolver) { + return resolver.resolve(invocables, locals, parent, self); + } + + var promises = []; + + angular.forEach(invocables, function(value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promises.push($q.resolve($injector.invoke(value))); + } else if (angular.isString(value)) { + promises.push($q.resolve($injector.get(value))); + } else { + promises.push($q.resolve(value)); + } + }); + + return $q.all(promises).then(function(resolves) { + var resolveObj = {}; + var resolveIter = 0; + angular.forEach(invocables, function(value, key) { + resolveObj[key] = resolves[resolveIter++]; + }); + + return resolveObj; + }); + } + }; + }]; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { + .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', + function($animate, $injector, $modalStack) { return { - restrict: 'EA', - replace: true, - templateUrl: 'template/modal/backdrop.html', - compile: function (tElement, tAttrs) { + restrict: 'A', + compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { - scope.animate = false; + if (attrs.modalInClass) { + $animate.addClass(element, attrs.modalInClass); - //trigger CSS transitions - $timeout(function () { - scope.animate = true; - }); + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + if (scope.modalOptions.animation) { + $animate.removeClass(element, attrs.modalInClass).then(done); + } else { + done(); + } + }); + } } }]) - .directive('modalWindow', ['$modalStack', '$q', function ($modalStack, $q) { + .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', + function($modalStack, $q, $animateCss, $document) { return { - restrict: 'EA', scope: { - index: '@', - animate: '=' + index: '@' }, - replace: true, + restrict: 'A', transclude: true, templateUrl: function(tElement, tAttrs) { - return tAttrs.templateUrl || 'template/modal/window.html'; + return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, - link: function (scope, element, attrs) { - element.addClass(attrs.windowClass || ''); + link: function(scope, element, attrs) { + element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; - scope.close = function (evt) { + scope.close = function(evt) { var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + if (modal && modal.value.backdrop && + modal.value.backdrop !== 'static' && + evt.target === evt.currentTarget) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; + // moved from template to fix issue #2280 + element.on('click', scope.close); + // This property is only added to the scope for the purpose of detecting when this directive is rendered. // We can detect that by using this property in the template associated with this directive and then use // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; - // Deferred object that will be resolved when this modal is render. + // Deferred object that will be resolved when this modal is rendered. var modalRenderDeferObj = $q.defer(); - // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. - // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. - attrs.$observe('modalRender', function (value) { - if (value == 'true') { - modalRenderDeferObj.resolve(); - } + // Resolve render promise post-digest + scope.$$postDigest(function() { + modalRenderDeferObj.resolve(); }); - modalRenderDeferObj.promise.then(function () { - // trigger CSS transitions - scope.animate = true; - - var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]'); - /** - * Auto-focusing of a freshly-opened modal element causes any child elements - * with the autofocus attribute to lose focus. This is an issue on touch - * based devices which will show and then hide the onscreen keyboard. - * Attempts to refocus the autofocus element via JavaScript will not reopen - * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus - * the modal element if the modal does not contain an autofocus element. - */ - if (inputsWithAutofocus.length) { - inputsWithAutofocus[0].focus(); - } else { - element[0].focus(); - } + modalRenderDeferObj.promise.then(function() { + var animationPromise = null; - // Notify {@link $modalStack} that modal is rendered. - var modal = $modalStack.getTop(); - if (modal) { - $modalStack.modalRendered(modal.key); + if (attrs.modalInClass) { + animationPromise = $animateCss(element, { + addClass: attrs.modalInClass + }).start(); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + $animateCss(element, { + removeClass: attrs.modalInClass + }).start().then(done); + }); } + + + $q.when(animationPromise).then(function() { + // Notify {@link $modalStack} that modal is rendered. + var modal = $modalStack.getTop(); + if (modal) { + $modalStack.modalRendered(modal.key); + } + + /** + * If something within the freshly-opened modal already has focus (perhaps via a + * directive that causes focus) then there's no need to try to focus anything. + */ + if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { + var inputWithAutofocus = element[0].querySelector('[autofocus]'); + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (inputWithAutofocus) { + inputWithAutofocus.focus(); + } else { + element[0].focus(); + } + } + }); }); } }; }]) - .directive('modalAnimationClass', [ - function () { - return { - compile: function (tElement, tAttrs) { - if (tAttrs.modalAnimation) { - tElement.addClass(tAttrs.modalAnimationClass); - } + .directive('uibModalAnimationClass', function() { + return { + compile: function(tElement, tAttrs) { + if (tAttrs.modalAnimation) { + tElement.addClass(tAttrs.uibModalAnimationClass); } - }; - }]) + } + }; + }) - .directive('modalTransclude', function () { + .directive('uibModalTransclude', ['$animate', function($animate) { return { - link: function($scope, $element, $attrs, controller, $transclude) { - $transclude($scope.$parent, function(clone) { - $element.empty(); - $element.append(clone); + link: function(scope, element, attrs, controller, transclude) { + transclude(scope.$parent, function(clone) { + element.empty(); + $animate.enter(clone, element); }); } }; - }) - - .factory('$modalStack', ['$animate', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', - function ($animate, $timeout, $document, $compile, $rootScope, $$stackedMap) { + }]) + .factory('$uibModalStack', ['$animate', '$animateCss', '$document', + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; + var openedClasses = $$multiMap.createNew(); + var $modalStack = { + NOW_CLOSING_EVENT: 'modal.stack.now-closing' + }; + var topModalIndex = 0; + var previousTopOpenedModal = null; + var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; + + //Modal focus behavior + var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + + 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' + + 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]'; + var scrollbarPadding; + var SNAKE_CASE_REGEXP = /[A-Z]/g; + + // TODO: extract into common dependency with tooltip + function snake_case(name) { + var separator = '-'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + function isVisible(element) { + return !!(element.offsetWidth || + element.offsetHeight || + element.getClientRects().length); + } function backdropIndex() { var topBackdropIndex = -1; @@ -2179,62 +4006,98 @@ angular.module('ui.bootstrap.modal', []) topBackdropIndex = i; } } + + // If any backdrop exist, ensure that it's index is always + // right below the top modal + if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { + topBackdropIndex = topModalIndex; + } return topBackdropIndex; } - $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + $rootScope.$watch(backdropIndex, function(newBackdropIndex) { if (backdropScope) { backdropScope.index = newBackdropIndex; } }); function removeModalWindow(modalInstance, elementToReceiveFocus) { - - var body = $document.find('body').eq(0); var modalWindow = openedWindows.get(modalInstance).value; + var appendToElement = modalWindow.appendTo; //clean up the stack openedWindows.remove(modalInstance); + previousTopOpenedModal = openedWindows.top(); + if (previousTopOpenedModal) { + topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); + } - //remove window DOM element removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - checkRemoveBackdrop(); - }); + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + var areAnyOpen = openedClasses.hasKey(modalBodyClass); + appendToElement.toggleClass(modalBodyClass, areAnyOpen); + if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); + } + scrollbarPadding = null; + } + toggleTopWindowClass(true); + }, modalWindow.closedDeferred); + checkRemoveBackdrop(); //move focus to specified element if available, or else to body if (elementToReceiveFocus && elementToReceiveFocus.focus) { elementToReceiveFocus.focus(); - } else { - body.focus(); + } else if (appendToElement.focus) { + appendToElement.focus(); } } - function checkRemoveBackdrop() { - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - var backdropScopeRef = backdropScope; - removeAfterAnimate(backdropDomEl, backdropScope, function () { - backdropScopeRef = null; - }); - backdropDomEl = undefined; - backdropScope = undefined; - } - } + // Add or remove "windowTopClass" from the top window in the stack + function toggleTopWindowClass(toggleSwitch) { + var modalWindow; - function removeAfterAnimate(domEl, scope, done) { - // Closing animation - scope.animate = false; + if (openedWindows.length() > 0) { + modalWindow = openedWindows.top().value; + modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); + } + } - if (domEl.attr('modal-animation') && $animate.enabled()) { - // transition out - domEl.one('$animate:close', function closeFn() { - $rootScope.$evalAsync(afterAnimating); + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() === -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, function() { + backdropScopeRef = null; }); - } else { - // Ensure this call is async - $timeout(afterAnimating); + backdropDomEl = undefined; + backdropScope = undefined; } + } + + function removeAfterAnimate(domEl, scope, done, closedDeferred) { + var asyncDeferred; + var asyncPromise = null; + var setIsAsync = function() { + if (!asyncDeferred) { + asyncDeferred = $q.defer(); + asyncPromise = asyncDeferred.promise; + } + + return function asyncDone() { + asyncDeferred.resolve(); + }; + }; + scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); + + // Note that it's intentional that asyncPromise might be null. + // That's when setIsAsync has not been called during the + // NOW_CLOSING_EVENT broadcast. + return $q.when(asyncPromise).then(afterAnimating); function afterAnimating() { if (afterAnimating.done) { @@ -2242,91 +4105,244 @@ angular.module('ui.bootstrap.modal', []) } afterAnimating.done = true; - domEl.remove(); + $animate.leave(domEl).then(function() { + if (done) { + done(); + } + + domEl.remove(); + if (closedDeferred) { + closedDeferred.resolve(); + } + }); + scope.$destroy(); - if (done) { - done(); - } } } - $document.bind('keydown', function (evt) { - var modal; + $document.on('keydown', keydownListener); - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - evt.preventDefault(); - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key, 'escape key press'); - }); + $rootScope.$on('$destroy', function() { + $document.off('keydown', keydownListener); + }); + + function keydownListener(evt) { + if (evt.isDefaultPrevented()) { + return evt; + } + + var modal = openedWindows.top(); + if (modal) { + switch (evt.which) { + case 27: { + if (modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function() { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + break; + } + case 9: { + var list = $modalStack.loadFocusElementList(modal); + var focusChanged = false; + if (evt.shiftKey) { + if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { + focusChanged = $modalStack.focusLastFocusableElement(list); + } + } else { + if ($modalStack.isFocusInLastItem(evt, list)) { + focusChanged = $modalStack.focusFirstFocusableElement(list); + } + } + + if (focusChanged) { + evt.preventDefault(); + evt.stopPropagation(); + } + + break; + } } } - }); + } + + $modalStack.open = function(modalInstance, modal) { + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; - $modalStack.open = function (modalInstance, modal) { + toggleTopWindowClass(false); - var modalOpener = $document[0].activeElement; + // Store the current top first, to determine what index we ought to use + // for the current top modal + previousTopOpenedModal = openedWindows.top(); openedWindows.add(modalInstance, { deferred: modal.deferred, renderDeferred: modal.renderDeferred, + closedDeferred: modal.closedDeferred, modalScope: modal.scope, backdrop: modal.backdrop, - keyboard: modal.keyboard + keyboard: modal.keyboard, + openedClass: modal.openedClass, + windowTopClass: modal.windowTopClass, + animation: modal.animation, + appendTo: modal.appendTo }); - var body = $document.find('body').eq(0), + openedClasses.put(modalBodyClass, modalInstance); + + var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); + backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; - var angularBackgroundDomEl = angular.element('
        '); - angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = angular.element('
        '); + backdropDomEl.attr({ + 'class': 'modal-backdrop', + 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}', + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }); + if (modal.backdropClass) { + backdropDomEl.addClass(modal.backdropClass); + } + if (modal.animation) { - angularBackgroundDomEl.attr('modal-animation', 'true'); + backdropDomEl.attr('modal-animation', 'true'); } - backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); - body.append(backdropDomEl); + $compile(backdropDomEl)(backdropScope); + $animate.enter(backdropDomEl, appendToElement); + if ($uibPosition.isScrollable(appendToElement)) { + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } + } + } + + var content; + if (modal.component) { + content = document.createElement(snake_case(modal.component.name)); + content = angular.element(content); + content.attr({ + resolve: '$resolve', + 'modal-instance': '$uibModalInstance', + close: '$close($value)', + dismiss: '$dismiss($value)' + }); + } else { + content = modal.content; } - var angularDomEl = angular.element('
        '); + // Set the top modal index based on the index of the previous top modal + topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; + var angularDomEl = angular.element('
        '); angularDomEl.attr({ + 'class': 'modal', 'template-url': modal.windowTemplateUrl, - 'window-class': modal.windowClass, + 'window-top-class': modal.windowTopClass, + 'role': 'dialog', + 'aria-labelledby': modal.ariaLabelledBy, + 'aria-describedby': modal.ariaDescribedBy, 'size': modal.size, - 'index': openedWindows.length() - 1, - 'animate': 'animate' - }).html(modal.content); + 'index': topModalIndex, + 'animate': 'animate', + 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}', + 'tabindex': -1, + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }).append(content); + if (modal.windowClass) { + angularDomEl.addClass(modal.windowClass); + } + if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; + appendToElement.addClass(modalBodyClass); + if (modal.scope) { + // we need to explicitly add the modal index to the modal scope + // because it is needed by ngStyle to compute the zIndex property. + modal.scope.$$topModalIndex = topModalIndex; + } + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); + + openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); + + applyAriaHidden(angularDomEl); + + function applyAriaHidden(el) { + if (!el || el[0].tagName === 'BODY') { + return; + } + + getSiblings(el).forEach(function(sibling) { + var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', + ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); + + if (!ariaHiddenCount) { + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + } + + sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); + sibling.setAttribute('aria-hidden', 'true'); + }); + + return applyAriaHidden(el.parent()); + + function getSiblings(el) { + var children = el.parent() ? el.parent().children() : []; + + return Array.prototype.filter.call(children, function(child) { + return child !== el[0]; + }); + } + } }; function broadcastClosing(modalWindow, resultOrReason, closing) { - return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + } + + function unhideBackgroundElements() { + Array.prototype.forEach.call( + document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), + function(hiddenEl) { + var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), + newHiddenCount = ariaHiddenCount - 1; + hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); + + if (!newHiddenCount) { + hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); + hiddenEl.removeAttribute('aria-hidden'); + } + } + ); } - $modalStack.close = function (modalInstance, result) { + $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, result, true)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } + return !modalWindow; }; - $modalStack.dismiss = function (modalInstance, reason) { + $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; @@ -2334,65 +4350,113 @@ angular.module('ui.bootstrap.modal', []) return !modalWindow; }; - $modalStack.dismissAll = function (reason) { + $modalStack.dismissAll = function(reason) { var topModal = this.getTop(); while (topModal && this.dismiss(topModal.key, reason)) { topModal = this.getTop(); } }; - $modalStack.getTop = function () { + $modalStack.getTop = function() { return openedWindows.top(); }; - $modalStack.modalRendered = function (modalInstance) { + $modalStack.modalRendered = function(modalInstance) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.renderDeferred.resolve(); } }; + $modalStack.focusFirstFocusableElement = function(list) { + if (list.length > 0) { + list[0].focus(); + return true; + } + return false; + }; + + $modalStack.focusLastFocusableElement = function(list) { + if (list.length > 0) { + list[list.length - 1].focus(); + return true; + } + return false; + }; + + $modalStack.isModalFocused = function(evt, modalWindow) { + if (evt && modalWindow) { + var modalDomEl = modalWindow.value.modalDomEl; + if (modalDomEl && modalDomEl.length) { + return (evt.target || evt.srcElement) === modalDomEl[0]; + } + } + return false; + }; + + $modalStack.isFocusInFirstItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[0]; + } + return false; + }; + + $modalStack.isFocusInLastItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[list.length - 1]; + } + return false; + }; + + $modalStack.loadFocusElementList = function(modalWindow) { + if (modalWindow) { + var modalDomE1 = modalWindow.value.modalDomEl; + if (modalDomE1 && modalDomE1.length) { + var elements = modalDomE1[0].querySelectorAll(tabbableSelector); + return elements ? + Array.prototype.filter.call(elements, function(element) { + return isVisible(element); + }) : elements; + } + } + }; + return $modalStack; }]) - .provider('$modal', function () { - + .provider('$uibModal', function() { var $modalProvider = { options: { animation: true, backdrop: true, //can also be false or 'static' keyboard: true }, - $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack', - function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) { - + $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', + function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : - $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl); - } - - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); - } - }); - return promisesArr; + $templateRequest(angular.isFunction(options.templateUrl) ? + options.templateUrl() : options.templateUrl); } - $modal.open = function (modalOptions) { + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); + var modalClosedDeferred = $q.defer(); var modalRenderDeferred = $q.defer(); //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = { result: modalResultDeferred.promise, opened: modalOpenedDeferred.promise, + closed: modalClosedDeferred.promise, rendered: modalRenderDeferred.promise, close: function (result) { return $modalStack.close(modalInstance, result); @@ -2405,287 +4469,428 @@ angular.module('ui.bootstrap.modal', []) //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions); modalOptions.resolve = modalOptions.resolve || {}; + modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } //verify options - if (!modalOptions.template && !modalOptions.templateUrl) { - throw new Error('One of template or templateUrl options is required.'); + if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of component or template or templateUrl options is required.'); } - var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + var templateAndResolvePromise; + if (modalOptions.component) { + templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null)); + } else { + templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + } + function resolveWithTemplate() { + return templateAndResolvePromise; + } - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + // Wait for the resolution of the existing promise chain. + // Then switch to our own combined promise dependency (regardless of how the previous modal fared). + // Then add to $modalStack and resolve opened. + // Finally clean up the chain variable if no subsequent modal has overwritten it. + var samePromise; + samePromise = promiseChain = $q.all([promiseChain]) + .then(resolveWithTemplate, resolveWithTemplate) + .then(function resolveSuccess(tplAndVars) { + var providedScope = modalOptions.scope || $rootScope; + + var modalScope = providedScope.$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + modalScope.$on('$destroy', function() { + if (!modalScope.$$uibDestructionScheduled) { + modalScope.$dismiss('$uibUnscheduledDestruction'); + } + }); - var modalScope = (modalOptions.scope || $rootScope).$new(); - modalScope.$close = modalInstance.close; - modalScope.$dismiss = modalInstance.dismiss; + var modal = { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + ariaLabelledBy: modalOptions.ariaLabelledBy, + ariaDescribedBy: modalOptions.ariaDescribedBy, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }; + + var component = {}; + var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; + + if (modalOptions.component) { + constructLocals(component, false, true, false); + component.name = modalOptions.component; + modal.component = component; + } else if (modalOptions.controller) { + constructLocals(ctrlLocals, true, false, true); + + // the third param will make the controller instantiate later,private api + // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 + ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs); + if (modalOptions.controllerAs && modalOptions.bindToController) { + ctrlInstance = ctrlInstantiate.instance; + ctrlInstance.$close = modalScope.$close; + ctrlInstance.$dismiss = modalScope.$dismiss; + angular.extend(ctrlInstance, { + $resolve: ctrlLocals.$scope.$resolve + }, providedScope); + } - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; + ctrlInstance = ctrlInstantiate(); - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; - }); + if (angular.isFunction(ctrlInstance.$onInit)) { + ctrlInstance.$onInit(); + } + } - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - if (modalOptions.controllerAs) { - modalScope[modalOptions.controllerAs] = ctrlInstance; + if (!modalOptions.component) { + modal.content = tplAndVars[0]; } - } - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - renderDeferred: modalRenderDeferred, - content: tplAndVars[0], - animation: modalOptions.animation, - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - backdropClass: modalOptions.backdropClass, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size - }); + $modalStack.open(modalInstance, modal); + modalOpenedDeferred.resolve(true); - }, function resolveError(reason) { - modalResultDeferred.reject(reason); - }); + function constructLocals(obj, template, instanceOnScope, injectable) { + obj.$scope = modalScope; + obj.$scope.$resolve = {}; + if (instanceOnScope) { + obj.$scope.$uibModalInstance = modalInstance; + } else { + obj.$uibModalInstance = modalInstance; + } + + var resolves = template ? tplAndVars[1] : tplAndVars; + angular.forEach(resolves, function(value, key) { + if (injectable) { + obj[key] = value; + } - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function (reason) { + obj.$scope.$resolve[key] = value; + }); + } + }, function resolveError(reason) { modalOpenedDeferred.reject(reason); + modalResultDeferred.reject(reason); + })['finally'](function() { + if (promiseChain === samePromise) { + promiseChain = null; + } }); return modalInstance; }; return $modal; - }] + } + ] }; return $modalProvider; }); -angular.module('ui.bootstrap.pagination', []) +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ +.factory('uibPaging', ['$parse', function($parse) { + return { + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + ctrl._watchers = []; -.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; + ngModelCtrl.$render = function() { + ctrl.render(); + }; - ngModelCtrl.$render = function() { - self.render(); - }; + if ($attrs.itemsPerPage) { + ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + })); + } else { + ctrl.itemsPerPage = config.itemsPerPage; + } - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; - if ( $scope.page > value ) { - $scope.selectPage(value); - } else { - ngModelCtrl.$render(); - } - }); - }; + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); + } - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); + } + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); + } + }; - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; - $scope.selectPage = function(page, evt) { - if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { - if (evt && evt.target) { - evt.target.blur(); - } - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); + $scope.noPrevious = function() { + return $scope.page === 1; + }; + + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.$on('$destroy', function() { + while (ctrl._watchers.length) { + ctrl._watchers.shift()(); + } + }); } }; +}]); - $scope.getText = function( key ) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) + +.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; + + uibPaging.create(this, $scope, $attrs); }]) -.constant('paginationConfig', { +.constant('uibPagerConfig', { itemsPerPage: 10, - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true + previousText: '« Previous', + nextText: 'Next »', + align: true }) -.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { +.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { return { - restrict: 'EA', scope: { totalItems: '=', - firstText: '@', previousText: '@', nextText: '@', - lastText: '@' + ngDisabled: '=' + }, + require: ['uibPager', '?ngModel'], + restrict: 'A', + controller: 'UibPagerController', + controllerAs: 'pager', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pager/pager.html'; }, - require: ['pagination', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pager'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { - return; // do nothing if no ng-model + return; // do nothing if no ng-model } - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } + paginationCtrl.init(ngModelCtrl, uibPagerConfig); + } + }; +}]); - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive - }; - } +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) +.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers, + pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + $attrs.$set('role', 'menu'); + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + })); + } - function getPages(currentPage, totalPages) { - var pages = []; + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + function getPages(currentPage, totalPages) { + var pages = []; - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); - endPage = startPage + maxSize - 1; + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); - } + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); - } + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, pageLabel(number), number === currentPage); + pages.push(page); + } - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); } - - return pages; } - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); } - }; + } + } + return pages; + } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); } }; }]) -.constant('pagerConfig', { +.constant('uibPaginationConfig', { itemsPerPage: 10, - previousText: '« Previous', - nextText: 'Next »', - align: true + boundaryLinks: false, + boundaryLinkNumbers: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true, + forceEllipses: false }) -.directive('pager', ['pagerConfig', function(pagerConfig) { +.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { return { - restrict: 'EA', scope: { totalItems: '=', + firstText: '@', previousText: '@', - nextText: '@' + nextText: '@', + lastText: '@', + ngDisabled:'=' + }, + require: ['uibPagination', '?ngModel'], + restrict: 'A', + controller: 'UibPaginationController', + controllerAs: 'pagination', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pagination/pagination.html'; }, - require: ['pager', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', - replace: true, link: function(scope, element, attrs, ctrls) { + element.addClass('pagination'); var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { return; // do nothing if no ng-model } - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); } }; }]); @@ -2695,18 +4900,20 @@ angular.module('ui.bootstrap.pagination', []) * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ -.provider( '$tooltip', function () { +.provider('$uibTooltip', function() { // The default options tooltip and popover. var defaultOptions = { placement: 'top', + placementClassPrefix: '', animation: true, popupDelay: 0, + popupCloseDelay: 0, useContentExp: false }; @@ -2714,7 +4921,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap var triggerMap = { 'mouseenter': 'mouseleave', 'click': 'click', - 'focus': 'blur' + 'outsideClick': 'outsideClick', + 'focus': 'blur', + 'none': '' }; // The options specified to the provider globally. @@ -2729,23 +4938,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * $tooltipProvider.options( { placement: 'left' } ); * }); */ - this.options = function( value ) { - angular.extend( globalOptions, value ); + this.options = function(value) { + angular.extend(globalOptions, value); }; /** * This allows you to extend the set of trigger mappings available. E.g.: * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); }; /** - * This is a helper function for translating camel-case to snake-case. + * This is a helper function for translating camel-case to snake_case. */ - function snake_case(name){ + function snake_case(name) { var regexp = /[A-Z]/g; var separator = '-'; return name.replace(regexp, function(letter, pos) { @@ -2757,9 +4966,26 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ - this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow, options ) { - options = angular.extend( {}, defaultOptions, globalOptions, options ); + this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keyup', keypressListener); + + $rootScope.$on('$destroy', function() { + $document.off('keyup', keypressListener); + }); + + function keypressListener(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + last = null; + } + } + } + + return function $tooltip(ttType, prefix, defaultTriggerShow, options) { + options = angular.extend({}, defaultOptions, globalOptions, options); /** * Returns an object of show and hide triggers. @@ -2775,57 +5001,102 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; + function getTriggers(trigger) { + var show = (trigger || options.trigger || defaultTriggerShow).split(' '); + var hide = show.map(function(trigger) { + return triggerMap[trigger] || trigger; + }); return { show: show, hide: hide }; } - var directiveName = snake_case( type ); + var directiveName = snake_case(ttType); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); var template = - '
        '+ + 'content="' + startSym + 'content' + endSym + '" ') + + 'origin-scope="origScope" ' + + 'class="uib-position-measure ' + prefix + '" ' + + 'tooltip-animation-class="fade"' + + 'uib-tooltip-classes ' + + 'ng-class="{ in: isOpen }" ' + + '>' + '
        '; return { - restrict: 'EA', - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); + compile: function(tElem, tAttrs) { + var tooltipLinker = $compile(template); - return function link ( scope, element, attrs, tooltipCtrl ) { + return function link(scope, element, attrs, tooltipCtrl) { var tooltip; var tooltipLinkedScope; var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var showTimeout; + var hideTimeout; + var positionTimeout; + var adjustmentTimeout; + var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; + var triggers = getTriggers(undefined); + var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); var ttScope = scope.$new(true); + var repositionScheduled = false; + var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; + var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; + var observers = []; + var lastPlacement; + + var positionTooltip = function() { + // check if tooltip exists and is not empty + if (!tooltip || !tooltip.html()) { return; } + + if (!positionTimeout) { + positionTimeout = $timeout(function() { + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var elementPos = appendToBody ? $position.offset(element) : $position.position(element); + tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); + var placementClasses = ttPosition.placement.split('-'); + + if (!tooltip.hasClass(placementClasses[0])) { + tooltip.removeClass(lastPlacement.split('-')[0]); + tooltip.addClass(placementClasses[0]); + } - var positionTooltip = function () { - if (!tooltip) { return; } + if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { + tooltip.removeClass(options.placementClassPrefix + lastPlacement); + tooltip.addClass(options.placementClassPrefix + ttPosition.placement); + } - var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; + adjustmentTimeout = $timeout(function() { + var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight); + if (adjustment) { + tooltip.css(adjustment); + } + adjustmentTimeout = null; + }, 0, false); + + // first time through tt element will have the + // uib-position-measure class or if the placement + // has changed we need to position the arrow. + if (tooltip.hasClass('uib-position-measure')) { + $position.positionArrow(tooltip, ttPosition.placement); + tooltip.removeClass('uib-position-measure'); + } else if (lastPlacement !== ttPosition.placement) { + $position.positionArrow(tooltip, ttPosition.placement); + } + lastPlacement = ttPosition.placement; - // Now set the calculated positioning. - tooltip.css( ttPosition ); + positionTimeout = null; + }, 0, false); + } }; // Set up the correct scope to allow transclusion later @@ -2835,8 +5106,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // TODO add ability to start tooltip opened ttScope.isOpen = false; - function toggleTooltipBind () { - if ( ! ttScope.isOpen ) { + function toggleTooltipBind() { + if (!ttScope.isOpen) { showTooltipBind(); } else { hideTooltipBind(); @@ -2845,213 +5116,360 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { return; } + cancelHide(); prepareTooltip(); - if ( ttScope.popupDelay ) { + if (ttScope.popupDelay) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, ttScope.popupDelay, false ); - popupTimeout.then(function(reposition){reposition();}); + if (!showTimeout) { + showTimeout = $timeout(show, ttScope.popupDelay, false); } } else { - show()(); + show(); } } - function hideTooltipBind () { - scope.$apply(function () { + function hideTooltipBind() { + cancelShow(); + + if (ttScope.popupCloseDelay) { + if (!hideTimeout) { + hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); + } + } else { hide(); - }); + } } // Show the tooltip popup element. function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } + cancelShow(); + cancelHide(); // Don't show empty tooltips. - if ( !(options.useContentExp ? ttScope.contentExp() : ttScope.content) ) { + if (!ttScope.content) { return angular.noop; } createTooltip(); - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - ttScope.$digest(); - - positionTooltip(); - // And show the tooltip. - ttScope.isOpen = true; - ttScope.$apply(); // digest required as $apply is not called + ttScope.$evalAsync(function() { + ttScope.isOpen = true; + assignIsOpen(true); + positionTooltip(); + }); + } + + function cancelShow() { + if (showTimeout) { + $timeout.cancel(showTimeout); + showTimeout = null; + } - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; + if (positionTimeout) { + $timeout.cancel(positionTimeout); + positionTimeout = null; + } } // Hide the tooltip popup element. function hide() { + if (!ttScope) { + return; + } + // First things first: we don't show it anymore. - ttScope.isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( ttScope.animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); + ttScope.$evalAsync(function() { + if (ttScope) { + ttScope.isOpen = false; + assignIsOpen(false); + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + // The fade transition in TWBS is 150ms. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 150, false); + } + } else { + removeTooltip(); + } } - } else { - removeTooltip(); + }); + } + + function cancelHide() { + if (hideTimeout) { + $timeout.cancel(hideTimeout); + hideTimeout = null; + } + + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; } } function createTooltip() { // There can only be one tooltip element per directive shown at once. if (tooltip) { - removeTooltip(); + return; } + tooltipLinkedScope = ttScope.$new(); - tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); } else { - element.after( tooltip ); + element.after(tooltip); } }); - tooltipLinkedScope.$watch(function () { - $timeout(positionTooltip, 0, false); + openedTooltips.add(ttScope, { + close: hide }); - if (options.useContentExp) { - tooltipLinkedScope.$watch('contentExp()', function (val) { - if (!val && ttScope.isOpen ) { - hide(); - } - }); - } + prepObservers(); } function removeTooltip() { - transitionTimeout = null; + cancelShow(); + cancelHide(); + unregisterObservers(); + if (tooltip) { tooltip.remove(); + tooltip = null; + if (adjustmentTimeout) { + $timeout.cancel(adjustmentTimeout); + } } + + openedTooltips.remove(ttScope); + if (tooltipLinkedScope) { tooltipLinkedScope.$destroy(); tooltipLinkedScope = null; } } + /** + * Set the initial scope values. Once + * the tooltip is created, the observers + * will be added to keep things in sync. + */ function prepareTooltip() { - prepPopupClass(); - prepPlacement(); - prepPopupDelay(); + ttScope.title = attrs[prefix + 'Title']; + if (contentParse) { + ttScope.content = contentParse(scope); + } else { + ttScope.content = attrs[ttType]; + } + + ttScope.popupClass = attrs[prefix + 'Class']; + ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; + var placement = $position.parsePlacement(ttScope.placement); + lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; + + var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); + var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; + } + + function assignIsOpen(isOpen) { + if (isOpenParse && angular.isFunction(isOpenParse.assign)) { + isOpenParse.assign(scope, isOpen); + } } - ttScope.contentExp = function () { - return scope.$eval(attrs[type]); + ttScope.contentExp = function() { + return ttScope.content; }; /** * Observe the relevant attributes. */ - if (!options.useContentExp) { - attrs.$observe( type, function ( val ) { - ttScope.content = val; + attrs.$observe('disabled', function(val) { + if (val) { + cancelShow(); + } + + if (val && ttScope.isOpen) { + hide(); + } + }); - if (!val && ttScope.isOpen ) { - hide(); + if (isOpenParse) { + scope.$watch(isOpenParse, function(val) { + if (ttScope && !val === ttScope.isOpen) { + toggleTooltipBind(); } }); } - attrs.$observe( 'disabled', function ( val ) { - if (val && ttScope.isOpen ) { - hide(); + function prepObservers() { + observers.length = 0; + + if (contentParse) { + observers.push( + scope.$watch(contentParse, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } + }) + ); + + observers.push( + tooltipLinkedScope.$watch(function() { + if (!repositionScheduled) { + repositionScheduled = true; + tooltipLinkedScope.$$postDigest(function() { + repositionScheduled = false; + if (ttScope && ttScope.isOpen) { + positionTooltip(); + } + }); + } + }) + ); + } else { + observers.push( + attrs.$observe(ttType, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } else { + positionTooltip(); + } + }) + ); } - }); - attrs.$observe( prefix+'Title', function ( val ) { - ttScope.title = val; - }); + observers.push( + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + + observers.push( + attrs.$observe(prefix + 'Placement', function(val) { + ttScope.placement = val ? val : options.placement; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + } - function prepPopupClass() { - ttScope.popupClass = attrs[prefix + 'Class']; + function unregisterObservers() { + if (observers.length) { + angular.forEach(observers, function(observer) { + observer(); + }); + observers.length = 0; + } } - function prepPlacement() { - var val = attrs[ prefix + 'Placement' ]; - ttScope.placement = angular.isDefined( val ) ? val : options.placement; + // hide tooltips/popovers for outsideClick trigger + function bodyHideTooltipBind(e) { + if (!ttScope || !ttScope.isOpen || !tooltip) { + return; + } + // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked + if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { + hideTooltipBind(); + } } - function prepPopupDelay() { - var val = attrs[ prefix + 'PopupDelay' ]; - var delay = parseInt( val, 10 ); - ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + // KeyboardEvent handler to hide the tooltip on Escape key press + function hideOnEscapeKey(e) { + if (e.which === 27) { + hideTooltipBind(); + } } - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); + var unregisterTriggers = function() { + triggers.show.forEach(function(trigger) { + if (trigger === 'outsideClick') { + element.off('click', toggleTooltipBind); + } else { + element.off(trigger, showTooltipBind); + element.off(trigger, toggleTooltipBind); + } + element.off('keypress', hideOnEscapeKey); + }); + triggers.hide.forEach(function(trigger) { + if (trigger === 'outsideClick') { + $document.off('click', bodyHideTooltipBind); + } else { + element.off(trigger, hideTooltipBind); + } + }); }; function prepTriggers() { - var val = attrs[ prefix + 'Trigger' ]; + var showTriggers = [], hideTriggers = []; + var val = scope.$eval(attrs[prefix + 'Trigger']); unregisterTriggers(); - triggers = getTriggers( val ); - - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); + if (angular.isObject(val)) { + Object.keys(val).forEach(function(key) { + showTriggers.push(key); + hideTriggers.push(val[key]); + }); + triggers = { + show: showTriggers, + hide: hideTriggers + }; } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); + triggers = getTriggers(val); + } + + if (triggers.show !== 'none') { + triggers.show.forEach(function(trigger, idx) { + if (trigger === 'outsideClick') { + element.on('click', toggleTooltipBind); + $document.on('click', bodyHideTooltipBind); + } else if (trigger === triggers.hide[idx]) { + element.on(trigger, toggleTooltipBind); + } else if (trigger) { + element.on(trigger, showTooltipBind); + element.on(triggers.hide[idx], hideTooltipBind); + } + element.on('keypress', hideOnEscapeKey); + }); } } + prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); - appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( ttScope.isOpen ) { - hide(); - } - }); + var appendToBodyVal; + var appendKey = prefix + 'AppendToBody'; + if (appendKey in attrs && attrs[appendKey] === undefined) { + appendToBodyVal = true; + } else { + appendToBodyVal = scope.$eval(attrs[appendKey]); } + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); ttScope = null; @@ -3064,11 +5482,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }) // This is mostly ngInclude code but with a custom scope -.directive( 'tooltipTemplateTransclude', [ +.directive('uibTooltipTemplateTransclude', [ '$animate', '$sce', '$compile', '$templateRequest', -function ($animate , $sce , $compile , $templateRequest) { +function ($animate, $sce, $compile, $templateRequest) { return { - link: function ( scope, elem, attrs ) { + link: function(scope, elem, attrs) { var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); var changeCounter = 0, @@ -3081,10 +5499,12 @@ function ($animate , $sce , $compile , $templateRequest) { previousElement.remove(); previousElement = null; } + if (currentScope) { currentScope.$destroy(); currentScope = null; } + if (currentElement) { $animate.leave(currentElement).then(function() { previousElement = null; @@ -3094,7 +5514,7 @@ function ($animate , $sce , $compile , $templateRequest) { } }; - scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) { + scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { var thisChangeId = ++changeCounter; if (src) { @@ -3136,219 +5556,242 @@ function ($animate , $sce , $compile , $templateRequest) { * They must not be animated as they're expected to be present on the tooltip on * initialization. */ -.directive('tooltipClasses', function () { +.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { return { restrict: 'A', - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + // need to set the primary position so the + // arrow has space during position measure. + // tooltip.positionTooltip() if (scope.placement) { - element.addClass(scope.placement); + // // There are no top-left etc... classes + // // in TWBS, so we need the primary position. + var position = $uibPosition.parsePlacement(scope.placement); + element.addClass(position[0]); } + if (scope.popupClass) { element.addClass(scope.popupClass); } - if (scope.animation()) { + + if (scope.animation) { element.addClass(attrs.tooltipAnimationClass); } } }; -}) - -.directive( 'tooltipPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) - -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); }]) -.directive( 'tooltipTemplatePopup', function () { +.directive('uibTooltipPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, - templateUrl: 'template/tooltip/tooltip-template-popup.html' + restrict: 'A', + scope: { content: '@' }, + templateUrl: 'uib/template/tooltip/tooltip-popup.html' }; }) -.directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) { - return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', { - useContentExp: true - }); +.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); }]) -.directive( 'tooltipHtmlPopup', function () { +.directive('uibTooltipTemplatePopup', function() { return { - restrict: 'EA', - replace: true, - scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-popup.html' + restrict: 'A', + scope: { contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' }; }) -.directive( 'tooltipHtml', [ '$tooltip', function ( $tooltip ) { - return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', { +.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { useContentExp: true }); }]) -/* -Deprecated -*/ -.directive( 'tooltipHtmlUnsafePopup', function () { +.directive('uibTooltipHtmlPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + restrict: 'A', + scope: { contentExp: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' }; }) -.value('tooltipHtmlUnsafeSuppressDeprecated', false) -.directive( 'tooltipHtmlUnsafe', [ - '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log', -function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) { - if (!tooltipHtmlUnsafeSuppressDeprecated) { - $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.'); - } - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { + useContentExp: true + }); }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. + * just mouse enter/leave, and selector delegatation. */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) +angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) -.directive( 'popoverTemplatePopup', function () { +.directive('uibPopoverTemplatePopup', function() { return { - restrict: 'EA', - replace: true, - scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', - originScope: '&' }, - templateUrl: 'template/popover/popover-template.html' + restrict: 'A', + scope: { uibTitle: '@', contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/popover/popover-template.html' }; }) -.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popoverTemplate', 'popover', 'click', { +.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { useContentExp: true - } ); + }); }]) -.directive( 'popoverPopup', function () { +.directive('uibPopoverHtmlPopup', function() { return { - restrict: 'EA', - replace: true, - scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' + restrict: 'A', + scope: { contentExp: '&', uibTitle: '@' }, + templateUrl: 'uib/template/popover/popover-html.html' + }; +}) + +.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverHtml', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverPopup', function() { + return { + restrict: 'A', + scope: { uibTitle: '@', content: '@' }, + templateUrl: 'uib/template/popover/popover.html' }; }) -.directive( 'popover', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); +.directive('uibPopover', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopover', 'popover', 'click'); }]); angular.module('ui.bootstrap.progressbar', []) -.constant('progressConfig', { +.constant('uibProgressConfig', { animate: true, max: 100 }) -.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - var self = this, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; +.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - this.bars = []; - $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max; + this.bars = []; + $scope.max = getMaxOrDefault(); - this.addBar = function(bar, element) { - if ( !animate ) { - element.css({'transition': 'none'}); - } + this.addBar = function(bar, element, attrs) { + if (!animate) { + element.css({'transition': 'none'}); + } - this.bars.push(bar); + this.bars.push(bar); - bar.$watch('value', function( value ) { - bar.percent = +(100 * value / $scope.max).toFixed(2); - }); + bar.max = getMaxOrDefault(); + bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; - bar.$on('$destroy', function() { - element = null; - self.removeBar(bar); - }); - }; + bar.$watch('value', function(value) { + bar.recalculatePercentage(); + }); + + bar.recalculatePercentage = function() { + var totalPercentage = self.bars.reduce(function(total, bar) { + bar.percent = +(100 * bar.value / bar.max).toFixed(2); + return total + bar.percent; + }, 0); - this.removeBar = function(bar) { - this.bars.splice(this.bars.indexOf(bar), 1); + if (totalPercentage > 100) { + bar.percent -= totalPercentage - 100; + } }; + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + this.bars.forEach(function (bar) { + bar.recalculatePercentage(); + }); + }; + + //$attrs.$observe('maxParam', function(maxParam) { + $scope.$watch('maxParam', function(maxParam) { + self.bars.forEach(function(bar) { + bar.max = getMaxOrDefault(); + bar.recalculatePercentage(); + }); + }); + + function getMaxOrDefault () { + return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max; + } }]) -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - templateUrl: 'template/progressbar/progress.html' - }; +.directive('uibProgress', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + require: 'uibProgress', + scope: { + maxParam: '=?max' + }, + templateUrl: 'uib/template/progressbar/progress.html' + }; }) -.directive('bar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - require: '^progress', - scope: { - value: '=', - max: '=?', - type: '@' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, element); - } - }; +.directive('uibBar', function() { + return { + replace: true, + transclude: true, + require: '^uibProgress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'uib/template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element, attrs); + } + }; }) -.directive('progressbar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - scope: { - value: '=', - max: '=?', - type: '@' - }, - templateUrl: 'template/progressbar/progressbar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, angular.element(element.children()[0])); - } - }; +.directive('uibProgressbar', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + scope: { + value: '=', + maxParam: '=?max', + type: '@' + }, + templateUrl: 'uib/template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); + } + }; }); angular.module('ui.bootstrap.rating', []) -.constant('ratingConfig', { +.constant('uibRatingConfig', { max: 5, stateOn: null, - stateOff: null + stateOff: null, + enableReset: true, + titles: ['one', 'two', 'three', 'four', 'five'] }) -.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { - var ngModelCtrl = { $setViewValue: angular.noop }; +.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }, + self = this; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; @@ -3358,33 +5801,49 @@ angular.module('ui.bootstrap.rating', []) if (angular.isNumber(value) && value << 0 !== value) { value = Math.round(value); } + return value; }); this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; - - var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : - new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + this.enableReset = angular.isDefined($attrs.enableReset) ? + $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; + + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); $scope.range = this.buildTemplateObjects(ratingStates); }; this.buildTemplateObjects = function(states) { for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); } return states; }; + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } + + return this.titles[index]; + }; + $scope.rate = function(value) { - if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { - ngModelCtrl.$setViewValue(value); + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value; + ngModelCtrl.$setViewValue(newViewValue); ngModelCtrl.$render(); } }; $scope.enter = function(value) { - if ( !$scope.readonly ) { + if (!$scope.readonly) { $scope.value = value; } $scope.onHover({value: value}); @@ -3399,227 +5858,167 @@ angular.module('ui.bootstrap.rating', []) if (/(37|38|39|40)/.test(evt.which)) { evt.preventDefault(); evt.stopPropagation(); - $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); } }; this.render = function() { $scope.value = ngModelCtrl.$viewValue; + $scope.title = self.getTitle($scope.value - 1); }; }]) -.directive('rating', function() { +.directive('uibRating', function() { return { - restrict: 'EA', - require: ['rating', 'ngModel'], + require: ['uibRating', 'ngModel'], + restrict: 'A', scope: { - readonly: '=?', + readonly: '=?readOnly', onHover: '&', onLeave: '&' }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', - replace: true, + controller: 'UibRatingController', + templateUrl: 'uib/template/rating/rating.html', link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - ratingCtrl.init( ngModelCtrl ); + ratingCtrl.init(ngModelCtrl); } }; }); -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - angular.module('ui.bootstrap.tabs', []) -.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { +.controller('UibTabsetController', ['$scope', function ($scope) { var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; + oldIndex; + ctrl.tabs = []; + + ctrl.select = function(index, evt) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect({ + $event: evt, + $selectedIndex: index + }); + if (evt && evt.isDefaultPrevented()) { + return; + } + previousSelected.tab.active = false; + } - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect({ + $event: evt + }); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isDefined(oldIndex)) { + ctrl.active = null; + oldIndex = null; } - }); - selectedTab.active = true; - selectedTab.onSelect(); + } }; ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1 && tab.active !== false) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); - } - else { - tab.active = false; + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); } }; ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected and not destroyed - if (tab.active && tabs.length > 1 && !destroyed) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); + var index; + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].tab === tab) { + index = i; + break; + } + } + + if (ctrl.tabs[index].index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); } - tabs.splice(index, 1); + + ctrl.tabs.splice(index, 1); }; + $scope.$watch('tabset.active', function(val) { + if (angular.isDefined(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + var destroyed; $scope.$on('$destroy', function() { destroyed = true; }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * @param {boolean=} justified Whether or not to use justified styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
        - - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
        -
        - */ -.directive('tabset', function() { +.directive('uibTabset', function() { return { - restrict: 'EA', transclude: true, replace: true, - scope: { + scope: {}, + bindToController: { + active: '=?', type: '@' }, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', + controller: 'UibTabsetController', + controllerAs: 'tabset', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tabset.html'; + }, link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; } }; }) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
        - - -
        - - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
        -
        - - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
        - */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', '$log', function($parse, $log) { +.directive('uibTab', ['$parse', function($parse) { return { - require: '^tabset', - restrict: 'EA', + require: '^uibTabset', replace: true, - templateUrl: 'template/tabs/tab.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tab.html'; + }, transclude: true, scope: { - active: '=?', heading: '@', + index: '=?', + classes: '@?', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' @@ -3627,56 +6026,58 @@ angular.module('ui.bootstrap.tabs', []) controller: function() { //Empty controller so other directives can require being 'under' a tab }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } + controllerAs: 'tab', + link: function(scope, elm, attrs, tabsetCtrl, transclude) { + scope.disabled = false; + if (attrs.disable) { + scope.$parent.$watch($parse(attrs.disable), function(value) { + scope.disabled = !! value; }); + } - scope.disabled = false; - if ( attrs.disable ) { - scope.$parent.$watch($parse(attrs.disable), function(value) { - scope.disabled = !! value; - }); + if (angular.isUndefined(attrs.index)) { + if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) { + scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1; + } else { + scope.index = 0; } + } - // Deprecation support of "disabled" parameter - // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677 - // This code is duplicated from the lines above to make it easy to remove once - // the feature has been completely deprecated - if ( attrs.disabled ) { - $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"'); - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); - } + if (angular.isUndefined(attrs.classes)) { + scope.classes = ''; + } - scope.select = function() { - if ( !scope.disabled ) { - scope.active = true; + scope.select = function(evt) { + if (!scope.disabled) { + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } } - }; - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - - //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. - scope.$transcludeFn = transclude; + tabsetCtrl.select(index, evt); + } }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; } }; }]) -.directive('tabHeadingTransclude', [function() { +.directive('uibTabHeadingTransclude', function() { return { restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { + require: '^uibTab', + link: function(scope, elm) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { elm.html(''); @@ -3685,14 +6086,14 @@ angular.module('ui.bootstrap.tabs', []) }); } }; -}]) +}) -.directive('tabContentTransclude', function() { +.directive('uibTabContentTransclude', function() { return { restrict: 'A', - require: '^tabset', + require: '^uibTabset', link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.tabContentTransclude); + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. @@ -3708,107 +6109,202 @@ angular.module('ui.bootstrap.tabs', []) }); } }; + function isTabHeading(node) { - return node.tagName && ( - node.hasAttribute('tab-heading') || - node.hasAttribute('data-tab-heading') || - node.tagName.toLowerCase() === 'tab-heading' || - node.tagName.toLowerCase() === 'data-tab-heading' + return node.tagName && ( + node.hasAttribute('uib-tab-heading') || + node.hasAttribute('data-uib-tab-heading') || + node.hasAttribute('x-uib-tab-heading') || + node.tagName.toLowerCase() === 'uib-tab-heading' || + node.tagName.toLowerCase() === 'data-uib-tab-heading' || + node.tagName.toLowerCase() === 'x-uib-tab-heading' || + node.tagName.toLowerCase() === 'uib:tab-heading' ); } -}) - -; +}); angular.module('ui.bootstrap.timepicker', []) -.constant('timepickerConfig', { +.constant('uibTimepickerConfig', { hourStep: 1, minuteStep: 1, + secondStep: 1, showMeridian: true, + showSeconds: false, meridians: null, readonlyInput: false, mousewheel: true, arrowkeys: true, - showSpinners: true + showSpinners: true, + templateUrl: 'uib/template/timepicker/timepicker.html' }) -.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { +.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { + var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl; var selected = new Date(), - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + watchers = [], + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS, + padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true; + + $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; + $element.removeAttr('tabindex'); - this.init = function( ngModelCtrl_, inputs ) { + this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; - ngModelCtrl.$formatters.unshift(function (modelValue) { - return modelValue ? new Date( modelValue ) : null; + ngModelCtrl.$formatters.unshift(function(modelValue) { + return modelValue ? new Date(modelValue) : null; }); var hoursInputEl = inputs.eq(0), - minutesInputEl = inputs.eq(1); + minutesInputEl = inputs.eq(1), + secondsInputEl = inputs.eq(2); + + hoursModelCtrl = hoursInputEl.controller('ngModel'); + minutesModelCtrl = minutesInputEl.controller('ngModel'); + secondsModelCtrl = secondsInputEl.controller('ngModel'); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + + if (mousewheel) { + this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); } var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; if (arrowkeys) { - this.setupArrowkeyEvents( hoursInputEl, minutesInputEl ); + this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; - this.setupInputEvents( hoursInputEl, minutesInputEl ); + this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); }; var hourStep = timepickerConfig.hourStep; if ($attrs.hourStep) { - $scope.$parent.$watch($parse($attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = +value; + })); } var minuteStep = timepickerConfig.minuteStep; if ($attrs.minuteStep) { - $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = +value; + })); + } + + var min; + watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) { + var dt = new Date(value); + min = isNaN(dt) ? undefined : dt; + })); + + var max; + watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) { + var dt = new Date(value); + max = isNaN(dt) ? undefined : dt; + })); + + var disabled = false; + if ($attrs.ngDisabled) { + watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { + disabled = value; + })); + } + + $scope.noIncrementHours = function() { + var incrementedSelected = addMinutes(selected, hourStep * 60); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementHours = function() { + var decrementedSelected = addMinutes(selected, -hourStep * 60); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementMinutes = function() { + var incrementedSelected = addMinutes(selected, minuteStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementMinutes = function() { + var decrementedSelected = addMinutes(selected, -minuteStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementSeconds = function() { + var incrementedSelected = addSeconds(selected, secondStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementSeconds = function() { + var decrementedSelected = addSeconds(selected, -secondStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noToggleMeridian = function() { + if (selected.getHours() < 12) { + return disabled || addMinutes(selected, 12 * 60) > max; + } + + return disabled || addMinutes(selected, -12 * 60) < min; + }; + + var secondStep = timepickerConfig.secondStep; + if ($attrs.secondStep) { + watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) { + secondStep = +value; + })); + } + + $scope.showSeconds = timepickerConfig.showSeconds; + if ($attrs.showSeconds) { + watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) { + $scope.showSeconds = !!value; + })); } // 12H / 24H mode $scope.showMeridian = timepickerConfig.showMeridian; if ($attrs.showMeridian) { - $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) { $scope.showMeridian = !!value; - if ( ngModelCtrl.$error.time ) { + if (ngModelCtrl.$error.time) { // Evaluate from template var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); refresh(); } } else { updateTemplate(); } - }); + })); } // Get $scope.hours in 24H mode if valid - function getHoursFromTemplate ( ) { - var hours = parseInt( $scope.hours, 10 ); - var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { + function getHoursFromTemplate() { + var hours = +$scope.hours; + var valid = $scope.showMeridian ? hours > 0 && hours < 13 : + hours >= 0 && hours < 24; + if (!valid || $scope.hours === '') { return undefined; } - if ( $scope.showMeridian ) { - if ( hours === 12 ) { + if ($scope.showMeridian) { + if (hours === 12) { hours = 0; } - if ( $scope.meridian === meridians[1] ) { + if ($scope.meridian === meridians[1]) { hours = hours + 12; } } @@ -3816,118 +6312,222 @@ angular.module('ui.bootstrap.timepicker', []) } function getMinutesFromTemplate() { - var minutes = parseInt($scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + var minutes = +$scope.minutes; + var valid = minutes >= 0 && minutes < 60; + if (!valid || $scope.minutes === '') { + return undefined; + } + return minutes; + } + + function getSecondsFromTemplate() { + var seconds = +$scope.seconds; + return seconds >= 0 && seconds < 60 ? seconds : undefined; } - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value.toString(); + function pad(value, noPad) { + if (value === null) { + return ''; + } + + return angular.isDefined(value) && value.toString().length < 2 && !noPad ? + '0' + value : value.toString(); } // Respond on mousewheel spin - this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; } //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); + var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; + return e.detail || delta > 0; }; - hoursInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + hoursInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); + } e.preventDefault(); }); - minutesInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + minutesInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); + } e.preventDefault(); }); + secondsInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); + } + e.preventDefault(); + }); }; // Respond on up/down arrowkeys - this.setupArrowkeyEvents = function( hoursInputEl, minutesInputEl ) { - hoursInputEl.bind('keydown', function(e) { - if ( e.which === 38 ) { // up - e.preventDefault(); - $scope.incrementHours(); - $scope.$apply(); - } - else if ( e.which === 40 ) { // down - e.preventDefault(); - $scope.decrementHours(); - $scope.$apply(); + this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + hoursInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementHours(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementHours(); + $scope.$apply(); + } } }); - minutesInputEl.bind('keydown', function(e) { - if ( e.which === 38 ) { // up - e.preventDefault(); - $scope.incrementMinutes(); - $scope.$apply(); + minutesInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementMinutes(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementMinutes(); + $scope.$apply(); + } } - else if ( e.which === 40 ) { // down - e.preventDefault(); - $scope.decrementMinutes(); - $scope.$apply(); + }); + + secondsInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementSeconds(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementSeconds(); + $scope.$apply(); + } } }); }; - this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { - if ( $scope.readonlyInput ) { + this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + if ($scope.readonlyInput) { $scope.updateHours = angular.noop; $scope.updateMinutes = angular.noop; + $scope.updateSeconds = angular.noop; return; } - var invalidate = function(invalidHours, invalidMinutes) { - ngModelCtrl.$setViewValue( null ); + var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { + ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', false); + } } + if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', false); + } + } + + if (angular.isDefined(invalidSeconds)) { + $scope.invalidSeconds = invalidSeconds; + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', false); + } } }; $scope.updateHours = function() { - var hours = getHoursFromTemplate(); + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); + + ngModelCtrl.$setDirty(); - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(true); + } else { + refresh('h'); + } } else { invalidate(true); } }; - hoursInputEl.bind('blur', function(e) { - if ( !$scope.invalidHours && $scope.hours < 10) { - $scope.$apply( function() { - $scope.hours = pad( $scope.hours ); + hoursInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.hours === null || $scope.hours === '') { + invalidate(true); + } else if (!$scope.invalidHours && $scope.hours < 10) { + $scope.$apply(function() { + $scope.hours = pad($scope.hours, !padHours); }); } }); $scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(undefined, true); + } else { + refresh('m'); + } } else { invalidate(undefined, true); } }; - minutesInputEl.bind('blur', function(e) { - if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + minutesInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.minutes === null) { + invalidate(undefined, true); + } else if (!$scope.invalidMinutes && $scope.minutes < 10) { + $scope.$apply(function() { + $scope.minutes = pad($scope.minutes); + }); + } + }); + + $scope.updateSeconds = function() { + var seconds = getSecondsFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(seconds)) { + selected.setSeconds(seconds); + refresh('s'); + } else { + invalidate(undefined, undefined, true); + } + }; + + secondsInputEl.on('blur', function(e) { + if (modelIsEmpty()) { + makeValid(); + } else if (!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply( function() { - $scope.minutes = pad( $scope.minutes ); + $scope.seconds = pad($scope.seconds); }); } }); @@ -3937,375 +6537,702 @@ angular.module('ui.bootstrap.timepicker', []) this.render = function() { var date = ngModelCtrl.$viewValue; - if ( isNaN(date) ) { + if (isNaN(date)) { ngModelCtrl.$setValidity('time', false); $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } else { - if ( date ) { + if (date) { selected = date; } - makeValid(); + + if (selected < min || selected > max) { + ngModelCtrl.$setValidity('time', false); + $scope.invalidHours = true; + $scope.invalidMinutes = true; + } else { + makeValid(); + } updateTemplate(); } }; // Call internally when we know that model is valid. - function refresh( keyboardChange ) { + function refresh(keyboardChange) { makeValid(); - ngModelCtrl.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); + ngModelCtrl.$setViewValue(new Date(selected)); + updateTemplate(keyboardChange); } function makeValid() { + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', true); + } + + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', true); + } + + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', true); + } + ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; + $scope.invalidSeconds = false; } - function updateTemplate( keyboardChange ) { - var hours = selected.getHours(), minutes = selected.getMinutes(); + function updateTemplate(keyboardChange) { + if (!ngModelCtrl.$modelValue) { + $scope.hours = null; + $scope.minutes = null; + $scope.seconds = null; + $scope.meridian = meridians[0]; + } else { + var hours = selected.getHours(), + minutes = selected.getMinutes(), + seconds = selected.getSeconds(); + + if ($scope.showMeridian) { + hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system + } - if ( $scope.showMeridian ) { - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system - } + $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours); + if (keyboardChange !== 'm') { + $scope.minutes = pad(minutes); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; - $scope.hours = keyboardChange === 'h' ? hours : pad(hours); - if (keyboardChange !== 'm') { - $scope.minutes = pad(minutes); + if (keyboardChange !== 's') { + $scope.seconds = pad(seconds); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours(), dt.getMinutes() ); + function addSecondsToSelected(seconds) { + selected = addSeconds(selected, seconds); refresh(); } - + + function addMinutes(selected, minutes) { + return addSeconds(selected, minutes*60); + } + + function addSeconds(date, seconds) { + var dt = new Date(date.getTime() + seconds * 1000); + var newDate = new Date(date); + newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); + return newDate; + } + + function modelIsEmpty() { + return ($scope.hours === null || $scope.hours === '') && + ($scope.minutes === null || $scope.minutes === '') && + (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === '')); + } + $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; - + $scope.incrementHours = function() { - addMinutes( hourStep * 60 ); + if (!$scope.noIncrementHours()) { + addSecondsToSelected(hourStep * 60 * 60); + } }; + $scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); + if (!$scope.noDecrementHours()) { + addSecondsToSelected(-hourStep * 60 * 60); + } }; + $scope.incrementMinutes = function() { - addMinutes( minuteStep ); + if (!$scope.noIncrementMinutes()) { + addSecondsToSelected(minuteStep * 60); + } }; + $scope.decrementMinutes = function() { - addMinutes( - minuteStep ); + if (!$scope.noDecrementMinutes()) { + addSecondsToSelected(-minuteStep * 60); + } + }; + + $scope.incrementSeconds = function() { + if (!$scope.noIncrementSeconds()) { + addSecondsToSelected(secondStep); + } + }; + + $scope.decrementSeconds = function() { + if (!$scope.noDecrementSeconds()) { + addSecondsToSelected(-secondStep); + } }; + $scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); + + if (!$scope.noToggleMeridian()) { + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); + } else { + $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; + } + } + }; + + $scope.blur = function() { + ngModelCtrl.$setTouched(); }; + + $scope.$on('$destroy', function() { + while (watchers.length) { + watchers.shift()(); + } + }); }]) -.directive('timepicker', function () { +.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { return { - restrict: 'EA', - require: ['timepicker', '?^ngModel'], - controller:'TimepickerController', - replace: true, + require: ['uibTimepicker', '?^ngModel'], + restrict: 'A', + controller: 'UibTimepickerController', + controllerAs: 'timepicker', scope: {}, - templateUrl: 'template/timepicker/timepicker.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || uibTimepickerConfig.templateUrl; + }, link: function(scope, element, attrs, ctrls) { var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - if ( ngModelCtrl ) { - timepickerCtrl.init( ngModelCtrl, element.find('input') ); + if (ngModelCtrl) { + timepickerCtrl.init(ngModelCtrl, element.find('input')); } } }; -}); +}]); -angular.module('ui.bootstrap.transition', []) +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) -.value('$transitionSuppressDeprecated', false) /** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing */ -.factory('$transition', [ - '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated', -function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) { - - if (!$transitionSuppressDeprecated) { - $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.'); - } - - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + .factory('uibTypeaheadParser', ['$parse', function($parse) { + // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + return { + parse: function(input) { + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); + } - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); + return { + itemName: match[3], + source: $parse(match[4]), + viewMapper: $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) + }; + } }; + }]) - if (endEventName) { - element.bind(endEventName, transitionEndHandler); + .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', + function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; } - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + minLength = !newVal && newVal !== 0 ? 1 : newVal; }); - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); - } - deferred.reject('Transition cancelled'); + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; }; - return deferred.promise; - }; + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + + //binding to a variable that indicates if there were no results after the query is completed + var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; + + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var parsedModel = $parse(attrs.ngModel); + var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); + var $setModelValue = function(scope, newValue) { + if (angular.isFunction(parsedModel(originalScope)) && + ngModelOptions.getOption('getterSetter')) { + return invokeModelSetter(scope, {$$$p: newValue}); } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + return parsedModel.assign(scope, newValue); + }; -/** - * A helper service that can parse typeahead's syntax (string provided by users) - * Extracted to a separate service for ease of unit testing - */ - .factory('typeaheadParser', ['$parse', function ($parse) { + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + var hasFocus; - return { - parse:function (input) { + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
        '); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; + //pop-up element used to display matches + var popUpEl = angular.element('
        '); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); } - }; -}]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$rootScope', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $rootScope, $position, typeaheadParser) { + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - var HOT_KEYS = [9, 13, 27, 38, 40]; + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; - //SUPPORTED ATTRIBUTES (OPTIONS) + var getMatchId = function(index) { + return popupId + '-option-' + index; + }; - //minimal no of characters that needs to be entered before typeahead kicks-in - var minLength = originalScope.$eval(attrs.typeaheadMinLength); - if (!minLength && minLength !== 0) { - minLength = 1; + // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. + // This attribute is added or removed automatically when the `activeIdx` changes. + scope.$watch('activeIdx', function(index) { + if (index < 0) { + element.removeAttr('aria-activedescendant'); + } else { + element.attr('aria-activedescendant', getMatchId(index)); } + }); - //minimal wait time after last character typed before typeahead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + var inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + } - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + return false; + }; - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + var getMatchesAsync = function(inputValue, evt) { + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + isNoResultsSetter(originalScope, false); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = inputValue === modelCtrl.$viewValue; + if (onCurrentRequest && hasFocus) { + if (matches && matches.length > 0) { + scope.activeIdx = focusFirst ? 0 : -1; + isNoResultsSetter(originalScope, false); + scope.matches.length = 0; + + //transform labels + for (var i = 0; i < matches.length; i++) { + locals[parserResult.itemName] = matches[i]; + scope.matches.push({ + id: getMatchId(i), + label: parserResult.viewMapper(scope, locals), + model: matches[i] + }); + } - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); + scope.query = inputValue; + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + recalculatePosition(); - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + element.attr('aria-expanded', true); - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); + } + } - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } + } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); + } + } + if (onCurrentRequest) { + isLoadingSetter(originalScope, false); + } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; - //INTERNAL VARIABLES + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); + scope.moveInProgress = false; + }, eventDebounceTime); - var hasFocus; + // Default progress type + scope.moveInProgress = false; - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); + debouncedRecalculate(); + } - //pop-up element used to display matches - var popUpEl = angular.element('
        '); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + // recalculate actual position and set new values to scope + // after digest loop is popup in right position + function recalculatePosition() { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top += element.prop('offsetHeight'); + } + + //we need to propagate user's query so we can higlight matches + scope.query = undefined; + + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; + + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); } + }; - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - }; + resetMatches(); - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; - // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. - // This attribute is added or removed automatically when the `activeIdx` changes. - scope.$watch('activeIdx', function(index) { - if (index < 0) { - element.removeAttr('aria-activedescendant'); - } else { - element.attr('aria-activedescendant', getMatchId(index)); - } + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals), + $event: evt }); - var getMatchesAsync = function(inputValue) { + resetMatches(); - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { + $timeout(function() { element[0].focus(); }, 0, false); + } + }; - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - var onCurrentRequest = (inputValue === modelCtrl.$viewValue); - if (onCurrentRequest && hasFocus) { - if (matches && matches.length > 0) { + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } - scope.activeIdx = focusFirst ? 0 : -1; - scope.matches.length = 0; + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); - //transform labels - for(var i=0; i 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(scope.activeIdx, evt); + } + }); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); + } + }); + + element.on('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.on('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); } - }, function(){ - resetMatches(); - isLoadingSetter(originalScope, false); }); - }; + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + hasFocus = false; + selected = false; + }); - resetMatches(); + // Keep reference to click handler to unbind it. + var dismissClickHandler = function(evt) { + // Issue #3973 + // Firefox treats right click as a click on document + if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { + resetMatches(); + if (!$rootScope.$$phase) { + originalScope.$digest(); + } + } + }; - //we need to propagate user's query so we can higlight matches - scope.query = undefined; + $document.on('click', dismissClickHandler); - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var timeoutPromise; + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } - var scheduleSearchWithTimeout = function(inputValue) { - timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); - }; + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); - var cancelPreviousTimeout = function() { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise); - } - }; + if (showHint) { + inputsContainer.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + + this.init = function(_modelCtrl) { + modelCtrl = _modelCtrl; + ngModelOptions = extractOptions(modelCtrl); + + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue - modelCtrl.$parsers.unshift(function (inputValue) { - + modelCtrl.$parsers.unshift(function(inputValue) { hasFocus = true; if (minLength === 0 || inputValue && inputValue.length >= minLength) { @@ -4323,20 +7250,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap if (isEditable) { return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } } - }); - modelCtrl.$formatters.push(function (modelValue) { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } + + modelCtrl.$setValidity('editable', false); + return undefined; + }); + modelCtrl.$formatters.push(function(modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; @@ -4348,182 +7274,144 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } if (inputFormatter) { - locals.$model = modelValue; return inputFormatter(originalScope, locals); - - } else { - - //it might happen that we don't have enough info to properly render input value - //we need to check for this situation and simply return model value if we can't apply custom formatting - locals[parserResult.itemName] = modelValue; - candidateViewValue = parserResult.viewMapper(originalScope, locals); - locals[parserResult.itemName] = undefined; - emptyViewValue = parserResult.viewMapper(originalScope, locals); - - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } - }); - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(originalScope, locals) - }); - - resetMatches(); - - //return focus to the input element if a match was selected via a mouse click event - // use timeout to avoid $rootScope:inprog error - $timeout(function() { element[0].focus(); }, 0, false); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } - - // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything - if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { - return; } - evt.preventDefault(); - - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } - }); + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); - element.bind('blur', function (evt) { - hasFocus = false; + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); + }; - // Keep reference to click handler to unbind it. - var dismissClickHandler = function (evt) { - if (element[0] !== evt.target) { - resetMatches(); - if (!$rootScope.$$phase) { - scope.$digest(); - } - } - }; - - $document.bind('click', dismissClickHandler); - - originalScope.$on('$destroy', function(){ - $document.unbind('click', dismissClickHandler); - if (appendToBody) { - $popup.remove(); - } - // Prevent jQuery cache memory leak - popUpEl.remove(); - }); + function extractOptions(ngModelCtrl) { + var ngModelOptions; - var $popup = $compile(popUpEl)(scope); + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; - if (appendToBody) { - $document.find('body').append($popup); - } else { - element.after($popup); + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; } + + return ngModelOptions; } - }; + }]) -}]) + .directive('uibTypeahead', function() { + return { + controller: 'UibTypeaheadController', + require: ['ngModel', 'uibTypeahead'], + link: function(originalScope, element, attrs, ctrls) { + ctrls[1].init(ctrls[0]); + } + }; + }) - .directive('typeaheadPopup', function () { + .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) { return { - restrict:'EA', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'&', - select:'&' + scope: { + matches: '=', + query: '=', + active: '=', + position: '&', + moveInProgress: '=', + select: '&', + assignIsOpen: '&', + debounce: '&' }, - replace:true, - templateUrl:'template/typeahead/typeahead-popup.html', - link:function (scope, element, attrs) { - + replace: true, + templateUrl: function(element, attrs) { + return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; + }, + link: function(scope, element, attrs) { scope.templateUrl = attrs.templateUrl; - scope.isOpen = function () { - return scope.matches.length > 0; + scope.isOpen = function() { + var isDropdownOpen = scope.matches.length > 0; + scope.assignIsOpen({ isOpen: isDropdownOpen }); + return isDropdownOpen; }; - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; + scope.isActive = function(matchIdx) { + return scope.active === matchIdx; }; - scope.selectActive = function (matchIdx) { + scope.selectActive = function(matchIdx) { scope.active = matchIdx; }; - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); + scope.selectMatch = function(activeIdx, evt) { + var debounce = scope.debounce(); + if (angular.isNumber(debounce) || angular.isObject(debounce)) { + $$debounce(function() { + scope.select({activeIdx: activeIdx, evt: evt}); + }, angular.isNumber(debounce) ? debounce : debounce['default']); + } else { + scope.select({activeIdx: activeIdx, evt: evt}); + } }; } }; - }) + }]) - .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) { + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { return { - restrict:'EA', - scope:{ - index:'=', - match:'=', - query:'=' + scope: { + index: '=', + match: '=', + query: '=' }, - link:function (scope, element, attrs) { - var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + link: function(scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; $templateRequest(tplUrl).then(function(tplContent) { - $compile(tplContent.trim())(scope, function(clonedElement){ - element.replaceWith(clonedElement); - }); + var tplEl = angular.element(tplContent.trim()); + element.replaceWith(tplEl); + $compile(tplEl)(scope); }); } }; }]) - .filter('typeaheadHighlight', function() { + .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if (!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if (!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); -!angular.$$csp() && angular.element(document).find('head').prepend(''); \ No newline at end of file + }]); +angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && angular.element(document).find('head').prepend(''); angular.$$uibCarouselCss = true; }); +angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerCss = true; }); +angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend(''); angular.$$uibPositionCss = true; }); +angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerpopupCss = true; }); +angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend(''); angular.$$uibTooltipCss = true; }); +angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibTimepickerCss = true; }); +angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend(''); angular.$$uibTypeaheadCss = true; }); \ No newline at end of file diff --git a/ui-bootstrap.min.js b/ui-bootstrap.min.js index 13a8d8d..7f2e167 100644 --- a/ui-bootstrap.min.js +++ b/ui-bootstrap.min.js @@ -2,8 +2,9 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.13.1-SNAPSHOT - 2015-10-06 + * Version: 2.5.0 - 2017-01-28 * License: MIT - */ -angular.module("ui.bootstrap",["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.transition","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.collapse",[]).directive("collapse",["$animate",function(a){return{link:function(b,c,d){function e(){c.removeClass("collapse").addClass("collapsing"),a.addClass(c,"in",{to:{height:c[0].scrollHeight+"px"}}).then(f)}function f(){c.removeClass("collapsing"),c.css({height:"auto"})}function g(){c.css({height:c[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing"),a.removeClass(c,"in",{to:{height:"0"}}).then(h)}function h(){c.css({height:"0"}),c.removeClass("collapsing"),c.addClass("collapse")}b.$watch(d.collapse,function(a){a?g():e()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,angular.noop))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b,this.close=a.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}).directive("dismissOnTimeout",["$timeout",function(a){return{require:"alert",link:function(b,c,d,e){a(function(){e.close()},parseInt(d.dismissOnTimeout,10))}}}]),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("CarouselController",["$scope","$element","$interval","$animate",function(a,b,c,d){function e(b,c,e){q||(angular.extend(b,{direction:e,active:!0}),angular.extend(l.currentSlide||{},{direction:e,active:!1}),d.enabled()&&!a.noTransition&&!a.$currentTransition&&b.$element&&(b.$element.data(o,b.direction),a.$currentTransition=!0,b.$element.one("$animate:close",function(){a.$currentTransition=null})),l.currentSlide=b,p=c,a.$evalAsync(function(){angular.extend(f(l.getCurrentIndex()),{fetch:!0})}),g())}function f(a){if(angular.isUndefined(m[a].index))return m[a];{var b;m.length}for(b=0;b0&&(j=c(i,b))}function h(){j&&(c.cancel(j),j=null)}function i(){var b=+a.interval;k&&!isNaN(b)&&b>0&&m.length?a.next():a.pause()}var j,k,l=this,m=l.slides=a.slides=[],n="uib-noTransition",o="uib-slideDirection",p=-1;l.currentSlide=null;var q=!1;l.select=a.select=function(b,c){var d=l.indexOfSlide(b);void 0===c&&(c=d>l.getCurrentIndex()?"next":"prev"),b&&b!==l.currentSlide&&!a.$currentTransition&&e(b,d,c)},a.$on("$destroy",function(){q=!0}),l.getCurrentIndex=function(){return l.currentSlide&&angular.isDefined(l.currentSlide.index)?+l.currentSlide.index:p},l.indexOfSlide=function(a){return angular.isDefined(a.index)?+a.index:m.indexOf(a)},a.next=function(){var a=(l.getCurrentIndex()+1)%m.length;return l.select(f(a),"next")},a.prev=function(){var a=l.getCurrentIndex()-1<0?m.length-1:l.getCurrentIndex()-1;return l.select(f(a),"prev")},a.isActive=function(a){return l.currentSlide===a},a.$watch("interval",g),a.$on("$destroy",h),a.play=function(){k||(k=!0,g())},a.pause=function(){a.noPause||(k=!1,h())},l.addSlide=function(b,c){b.$element=c,m.push(b),1===m.length||b.active?(l.select(m[m.length-1]),1==m.length&&a.play()):b.active=!1},l.removeSlide=function(a){angular.isDefined(a.index)&&m.sort(function(a,b){return+a.index>+b.index});var b=m.indexOf(a);m.splice(b,1),m.length>0&&a.active?l.select(b>=m.length?m[b-1]:m[b]):p>b&&p--},l.preFetchSlides=function(){var a=l.getCurrentIndex(),b=(a+1)%m.length,c=0>a-1?m.length-1:a-1;angular.extend(f(a),{fetch:!0}),angular.extend(f(b),{fetch:!0}),angular.extend(f(c),{fetch:!0})},a.$watch("noTransition",function(a){b.data(n,a)})}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?",index:"=?",fetch:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}).animation(".item",["$animate",function(a){var b="uib-noTransition",c="uib-slideDirection";return{beforeAddClass:function(d,e,f){if("active"==e&&d.parent()&&!d.parent().data(b)){var g=!1,h=d.data(c),i="next"==h?"left":"right";return d.addClass(h),a.addClass(d,i).then(function(){g||d.removeClass(i+" "+h),f()}),function(){g=!0}}f()},beforeRemoveClass:function(d,e,f){if("active"==e&&d.parent()&&!d.parent().data(b)){var g=!1,h=d.data(c),i="next"==h?"left":"right";return a.addClass(d,i).then(function(){g||d.removeClass(i),f()}),function(){g=!0}}f()}}}]),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a){var c=[],d=a.split("");return angular.forEach(f,function(b,e){var f=a.indexOf(e);if(f>-1){a=a.split(""),d[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+e.length;h>g;g++)d[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+d.join("")+"$"),map:b(c,"index")}}function d(a,b,c){return 1>c?!1:1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}var e=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.parsers={};var f={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")},HH:{regex:"(?:0|1)[0-9]|2[0-3]",apply:function(a){this.hours=+a}},H:{regex:"1?[0-9]|2[0-3]",apply:function(a){this.hours=+a}},mm:{regex:"[0-5][0-9]",apply:function(a){this.minutes=+a}},m:{regex:"[0-9]|[1-5][0-9]",apply:function(a){this.minutes=+a}},sss:{regex:"[0-9][0-9][0-9]",apply:function(a){this.milliseconds=+a}},ss:{regex:"[0-5][0-9]",apply:function(a){this.seconds=+a}},s:{regex:"[0-9]|[1-5][0-9]",apply:function(a){this.seconds=+a}}};this.parse=function(b,f,g){if(!angular.isString(b)||!f)return b;f=a.DATETIME_FORMATS[f]||f,f=f.replace(e,"\\$&"),this.parsers[f]||(this.parsers[f]=c(f));var h=this.parsers[f],i=h.regex,j=h.map,k=b.match(i);if(k&&k.length){var l,m;l=g?{year:g.getFullYear(),month:g.getMonth(),date:g.getDate(),hours:g.getHours(),minutes:g.getMinutes(),seconds:g.getSeconds(),milliseconds:g.getMilliseconds()}:{year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0};for(var n=1,o=k.length;o>n;n++){var p=j[n-1];p.apply&&p.apply.call(l,k[n])}return d(l.year,l.month,l.date)&&(m=new Date(l.year,l.month,l.date,l.hours,l.minutes,l.seconds,l.milliseconds||0)),m}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null,shortcutPropagation:!1}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange","shortcutPropagation"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.maxMode=i.maxMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),angular.isDefined(b.initDate)?(this.activeDate=a.$parent.$eval(b.initDate)||new Date,a.$parent.$watch(b.initDate,function(a){a&&(j.$isEmpty(j.$modelValue)||j.$invalid)&&(i.activeDate=a,i.refreshView())})):this.activeDate=new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$viewValue){var a=new Date(j.$viewValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$viewValue?new Date(j.$viewValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$viewValue?new Date(j.$viewValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date),customClass:this.customClass(a)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$viewValue?new Date(j.$viewValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),i.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&",customClass:"&",shortcutPropagation:"&?"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=(11-e.startingDay)%7,p=b.rows.length,q=0;p>q;q++)b.weekNumbers.push(h(b.rows[q][o].date))}},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},e.handleKeyDown=function(a){var b=e.activeDate.getDate();if("left"===a)b-=1;else if("up"===a)b-=7;else if("right"===a)b+=1;else if("down"===a)b+=7;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getMonth()+("pageup"===a?-1:1);e.activeDate.setMonth(c,1),b=Math.min(f(e.activeDate.getFullYear(),e.activeDate.getMonth()),b)}else"home"===a?b=1:"end"===a&&(b=f(e.activeDate.getFullYear(),e.activeDate.getMonth()));e.activeDate.setDate(b)},e.refreshView()}}}]).directive("monthpicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/month.html",require:"^datepicker",link:function(b,c,d,e){e.step={years:1},e.element=c,e._refreshView=function(){for(var c=new Array(12),d=e.activeDate.getFullYear(),f=0;12>f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&",customClass:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(angular.isNumber(a)&&(a=new Date(a)),a){if(angular.isDate(a)&&!isNaN(a))return a;if(angular.isString(a)){var b=f.parse(a,o,h.date)||new Date(a);return isNaN(b)?void 0:b}return void 0}return null}function n(a,b){var c=a||b;if(angular.isNumber(c)&&(c=new Date(c)),c){if(angular.isDate(c)&&!isNaN(c))return!0;if(angular.isString(c)){var d=f.parse(c,o)||new Date(c);return!isNaN(d)}return!1}return!0}var o,p=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,q=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]};var r=!1;if(g.html5Types[j.type]?(o=g.html5Types[j.type],r=!0):(o=j.datepickerPopup||g.datepickerPopup,j.$observe("datepickerPopup",function(a){var b=a||g.datepickerPopup;if(b!==o&&(o=b,k.$modelValue=null,!o))throw new Error("datepickerPopup must have a date format specified.")})),!o)throw new Error("datepickerPopup must have a date format specified.");if(r&&j.datepickerPopup)throw new Error("HTML5 date input types do not support custom formats.");var s=angular.element("
        ");s.attr({"ng-model":"date","ng-change":"dateSelection()"});var t=angular.element(s.children()[0]);if(r&&"month"==j.type&&(t.attr("datepicker-mode",'"month"'),t.attr("min-mode","month")),j.datepickerOptions){var u=h.$parent.$eval(j.datepickerOptions);u.initDate&&(h.initDate=u.initDate,t.attr("init-date","initDate"),delete u.initDate),angular.forEach(u,function(a,b){t.attr(l(b),a)})}h.watchData={},angular.forEach(["minDate","maxDate","datepickerMode","initDate","shortcutPropagation"],function(a){if(j[a]){var c=b(j[a]);if(h.$parent.$watch(c,function(b){h.watchData[a]=b}),t.attr(l(a),"watchData."+a),"datepickerMode"===a){var d=c.assign;h.$watch("watchData."+a,function(a,b){angular.isFunction(d)&&a!==b&&d(h.$parent,a)})}}}),j.dateDisabled&&t.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),j.showWeeks&&t.attr("show-weeks",j.showWeeks),j.customClass&&t.attr("custom-class","customClass({ date: date, mode: mode })"),r?k.$formatters.push(function(a){return h.date=a,a}):(k.$$parserName="date",k.$validators.date=n,k.$parsers.unshift(m),k.$formatters.push(function(a){return h.date=a,k.$isEmpty(a)?a:e(a,o)})),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a);var b=h.date?e(h.date,o):"";i.val(b),k.$setViewValue(b),p&&(h.isOpen=!1,i[0].focus())},k.$viewChangeListeners.push(function(){h.date=f.parse(k.$viewValue,o,h.date)||new Date(k.$viewValue)});var v=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},w=function(a){h.keydown(a)};i.bind("keydown",w),h.keydown=function(a){27===a.which?(a.preventDefault(),h.isOpen&&a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=q?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",v)):c.unbind("click",v)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(h.date)?(a=new Date(h.date),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var x=a(s)(h);s.remove(),q?c.find("body").append(x):i.after(x),h.$on("$destroy",function(){x.remove(),i.unbind("keydown",w),c.unbind("click",v)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.position"]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document","$rootScope",function(a,b){var c=null;this.open=function(b){c||(a.bind("click",d),a.bind("keydown",e)),c&&c!==b&&(c.isOpen=!1),c=b},this.close=function(b){c===b&&(c=null,a.unbind("click",d),a.unbind("keydown",e))};var d=function(a){if(c&&(!a||"disabled"!==c.getAutoClose())){var d=c.getToggleElement();if(!(a&&d&&d[0].contains(a.target))){var e=c.getElement();a&&"outsideClick"===c.getAutoClose()&&e&&e[0].contains(a.target)||(c.isOpen=!1,b.$$phase||c.$apply())}}},e=function(a){27===a.which&&(c.focusToggleElement(),d())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate","$position","$document","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i,j){var k,l,m=this,n=a.$new(),o=d.openClass,p=angular.noop,q=b.onToggle?c(b.onToggle):angular.noop,r=!1;this.init=function(d){m.$element=d,b.isOpen&&(l=c(b.isOpen),p=l.assign,a.$watch(l,function(a){n.isOpen=!!a})),r=angular.isDefined(b.dropdownAppendToBody),r&&m.dropdownMenu&&(h.find("body").append(m.dropdownMenu),d.on("$destroy",function(){m.dropdownMenu.remove()}))},this.toggle=function(a){return n.isOpen=arguments.length?!!a:!n.isOpen},this.isOpen=function(){return n.isOpen},n.getToggleElement=function(){return m.toggleElement},n.getAutoClose=function(){return b.autoClose||"always"},n.getElement=function(){return m.$element},n.focusToggleElement=function(){m.toggleElement&&m.toggleElement[0].focus()},n.$watch("isOpen",function(b,c){if(r&&m.dropdownMenu){var d=g.positionElements(m.$element,m.dropdownMenu,"bottom-left",!0);m.dropdownMenu.css({top:d.top+"px",left:d.left+"px",display:b?"block":"none"})}if(f[b?"addClass":"removeClass"](m.$element,o),b)m.dropdownMenuTemplateUrl&&j(m.dropdownMenuTemplateUrl).then(function(a){k=n.$new(),i(a.trim())(k,function(a){var b=a;m.dropdownMenu.replaceWith(b),m.dropdownMenu=b})}),n.focusToggleElement(),e.open(n);else{if(m.dropdownMenuTemplateUrl){k&&k.$destroy();var h=angular.element('');m.dropdownMenu.replaceWith(h),m.dropdownMenu=h}e.close(n)}p(a,b),angular.isDefined(b)&&b!==c&&q(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){"disabled"!==n.getAutoClose()&&(n.isOpen=!1)}),a.$on("$destroy",function(){n.$destroy()})}]).directive("dropdown",function(){return{controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownMenu",function(){return{restrict:"AC",require:"?^dropdown",link:function(a,b,c,d){if(d){var e=c.templateUrl;e&&(d.dropdownMenuTemplateUrl=e),d.dropdownMenu||(d.dropdownMenu=b)}}}}).directive("dropdownToggle",function(){return{require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),i()}),b&&b.focus?b.focus():d.focus()}function i(){if(l&&-1==g()){var a=m;j(l,m,function(){a=null}),l=void 0,m=void 0}}function j(c,d,f){function g(){g.done||(g.done=!0,c.remove(),d.$destroy(),f&&f())}d.animate=!1,c.attr("modal-animation")&&a.enabled()?c.one("$animate:close",function(){e.$evalAsync(g)}):b(g)}function k(a,b,c){return!a.value.modalScope.$broadcast("modal.closing",b,c).defaultPrevented}var l,m,n="modal-open",o=f.createNew(),p={};return e.$watch(g,function(a){m&&(m.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=o.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){p.dismiss(b.key,"escape key press")})))}),p.open=function(a,b){var f=c[0].activeElement;o.add(a,{deferred:b.deferred,renderDeferred:b.renderDeferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var h=c.find("body").eq(0),i=g();if(i>=0&&!l){m=e.$new(!0),m.index=i;var j=angular.element('
        ');j.attr("backdrop-class",b.backdropClass),b.animation&&j.attr("modal-animation","true"),l=d(j)(m),h.append(l)}var k=angular.element('
        ');k.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:o.length()-1,animate:"animate"}).html(b.content),b.animation&&k.attr("modal-animation","true");var p=d(k)(b.scope);o.top().value.modalDomEl=p,o.top().value.modalOpener=f,h.append(p),h.addClass(n)},p.close=function(a,b){var c=o.get(a);return c&&k(c,b,!0)?(c.value.deferred.resolve(b),h(a,c.value.modalOpener),!0):!c},p.dismiss=function(a,b){var c=o.get(a);return c&&k(c,b,!1)?(c.value.deferred.reject(b),h(a,c.value.modalOpener),!0):!c},p.dismissAll=function(a){for(var b=this.getTop();b&&this.dismiss(b.key,a);)b=this.getTop()},p.getTop=function(){return o.top()},p.modalRendered=function(a){var b=o.get(a);b&&b.value.renderDeferred.resolve()},p}]).provider("$modal",function(){var a={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$templateRequest","$controller","$modalStack",function(b,c,d,e,f,g){function h(a){return a.template?d.when(a.template):e(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl)}function i(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var j={};return j.open=function(b){var e=d.defer(),j=d.defer(),k=d.defer(),l={result:e.promise,opened:j.promise,rendered:k.promise,close:function(a){ -return g.close(l,a)},dismiss:function(a){return g.dismiss(l,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var m=d.all([h(b)].concat(i(b.resolve)));return m.then(function(a){var d=(b.scope||c).$new();d.$close=l.close,d.$dismiss=l.dismiss;var h,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=l,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),h=f(b.controller,i),b.controllerAs&&(d[b.controllerAs]=h)),g.open(l,{scope:d,deferred:e,renderDeferred:k,content:a[0],animation:b.animation,backdrop:b.backdrop,keyboard:b.keyboard,backdropClass:b.backdropClass,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),m.then(function(){j.resolve(!0)},function(a){j.reject(a)}),l},j}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(g,h){e=g,this.config=h,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=h.itemsPerPage,a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b,c){a.page!==b&&b>0&&b<=a.totalPages&&(c&&c.target&&c.target.blur(),e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages}}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0,useContentExp:!1},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$document","$position","$interpolate",function(e,f,g,h,i,j){return function(e,k,l,m){function n(a){var b=a||m.trigger||l,d=c[b]||b;return{show:b,hide:d}}m=angular.extend({},b,d,m);var o=a(e),p=j.startSymbol(),q=j.endSymbol(),r="
        ';return{restrict:"EA",compile:function(){var a=f(r);return function(b,c,d){function f(){E.isOpen?l():j()}function j(){(!D||b.$eval(d[k+"Enable"]))&&(s(),E.popupDelay?A||(A=g(o,E.popupDelay,!1),A.then(function(a){a()})):o()())}function l(){b.$apply(function(){p()})}function o(){return A=null,z&&(g.cancel(z),z=null),(m.useContentExp?E.contentExp():E.content)?(q(),x.css({top:0,left:0,display:"block"}),E.$digest(),F(),E.isOpen=!0,E.$apply(),F):angular.noop}function p(){E.isOpen=!1,g.cancel(A),A=null,E.animation?z||(z=g(r,500)):r()}function q(){x&&r(),y=E.$new(),x=a(y,function(a){B?h.find("body").append(a):c.after(a)}),y.$watch(function(){g(F,0,!1)}),m.useContentExp&&y.$watch("contentExp()",function(a){!a&&E.isOpen&&p()})}function r(){z=null,x&&(x.remove(),x=null),y&&(y.$destroy(),y=null)}function s(){t(),u(),v()}function t(){E.popupClass=d[k+"Class"]}function u(){var a=d[k+"Placement"];E.placement=angular.isDefined(a)?a:m.placement}function v(){var a=d[k+"PopupDelay"],b=parseInt(a,10);E.popupDelay=isNaN(b)?m.popupDelay:b}function w(){var a=d[k+"Trigger"];G(),C=n(a),C.show===C.hide?c.bind(C.show,f):(c.bind(C.show,j),c.bind(C.hide,l))}var x,y,z,A,B=angular.isDefined(m.appendToBody)?m.appendToBody:!1,C=n(void 0),D=angular.isDefined(d[k+"Enable"]),E=b.$new(!0),F=function(){if(x){var a=i.positionElements(c,x,E.placement,B);a.top+="px",a.left+="px",x.css(a)}};E.origScope=b,E.isOpen=!1,E.contentExp=function(){return b.$eval(d[e])},m.useContentExp||d.$observe(e,function(a){E.content=a,!a&&E.isOpen&&p()}),d.$observe("disabled",function(a){a&&E.isOpen&&p()}),d.$observe(k+"Title",function(a){E.title=a});var G=function(){c.unbind(C.show,j),c.unbind(C.hide,l)};w();var H=b.$eval(d[k+"Animation"]);E.animation=angular.isDefined(H)?!!H:m.animation;var I=b.$eval(d[k+"AppendToBody"]);B=angular.isDefined(I)?I:B,B&&b.$on("$locationChangeSuccess",function(){E.isOpen&&p()}),b.$on("$destroy",function(){g.cancel(z),g.cancel(A),G(),r(),E=null})}}}}}]}).directive("tooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(a,b,c,d){return{link:function(e,f,g){var h,i,j,k=e.$eval(g.tooltipTemplateTranscludeScope),l=0,m=function(){i&&(i.remove(),i=null),h&&(h.$destroy(),h=null),j&&(a.leave(j).then(function(){i=null}),i=j,j=null)};e.$watch(b.parseAsResourceUrl(g.tooltipTemplateTransclude),function(b){var g=++l;b?(d(b,!0).then(function(d){if(g===l){var e=k.$new(),i=d,n=c(i)(e,function(b){m(),a.enter(b,f)});h=e,j=n,h.$emit("$includeContentLoaded",b)}},function(){g===l&&(m(),e.$emit("$includeContentError",b))}),e.$emit("$includeContentRequested",b)):m()}),e.$on("$destroy",m)}}}]).directive("tooltipClasses",function(){return{restrict:"A",link:function(a,b,c){a.placement&&b.addClass(a.placement),a.popupClass&&b.addClass(a.popupClass),a.animation()&&b.addClass(c.tooltipAnimationClass)}}}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipTemplatePopup",function(){return{restrict:"EA",replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/tooltip/tooltip-template-popup.html"}}).directive("tooltipTemplate",["$tooltip",function(a){return a("tooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("tooltipHtmlPopup",function(){return{restrict:"EA",replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-popup.html"}}).directive("tooltipHtml",["$tooltip",function(a){return a("tooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).value("tooltipHtmlUnsafeSuppressDeprecated",!1).directive("tooltipHtmlUnsafe",["$tooltip","tooltipHtmlUnsafeSuppressDeprecated","$log",function(a,b,c){return b||c.warn("tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead."),a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverTemplatePopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/popover/popover-template.html"}}).directive("popoverTemplate",["$tooltip",function(a){return a("popoverTemplate","popover","click",{useContentExp:!0})}]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(a.max)?a.max:c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",max:"=?",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",max:"=?",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,d.$formatters.push(function(a){return angular.isNumber(a)&&a<<0!==a&&(a=Math.round(a)),a}),this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length&&a.active!==!1?a.active=!0:a.active?b.select(a):a.active=!1},b.removeTab=function(a){var e=c.indexOf(a);if(a.active&&c.length>1&&!d){var f=e==c.length-1?e-1:e+1;b.select(c[f])}c.splice(e,1)};var d;a.$on("$destroy",function(){d=!0})}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse","$log",function(a,b){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(c,d,e){return function(c,d,f,g){c.$watch("active",function(a){a&&g.select(c)}),c.disabled=!1,f.disable&&c.$parent.$watch(a(f.disable),function(a){c.disabled=!!a}),f.disabled&&(b.warn('Use of "disabled" attribute has been deprecated, please use "disable"'),c.$parent.$watch(a(f.disabled),function(a){c.disabled=!!a})),c.select=function(){c.disabled||(c.active=!0)},g.addTab(c),c.$on("$destroy",function(){g.removeTab(c)}),c.$transcludeFn=e}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0,arrowkeys:!0,showSpinners:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a.toString()}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),"m"!==b&&(a.minutes=i(d)),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render,o.$formatters.unshift(function(a){return a?new Date(a):null});var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g);var i=angular.isDefined(b.arrowkeys)?a.$parent.$eval(b.arrowkeys):f.arrowkeys;i&&this.setupArrowkeyEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupArrowkeyEvents=function(b,c){b.bind("keydown",function(b){38===b.which?(b.preventDefault(),a.incrementHours(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementHours(),a.$apply())}),c.bind("keydown",function(b){38===b.which?(b.preventDefault(),a.incrementMinutes(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementMinutes(),a.$apply())})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$viewValue;isNaN(a)?(o.$setValidity("time",!1),d.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(n=a),k(),l())},a.showSpinners=angular.isDefined(b.showSpinners)?a.$parent.$eval(b.showSpinners):f.showSpinners,a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.transition",[]).value("$transitionSuppressDeprecated",!1).factory("$transition",["$q","$timeout","$rootScope","$log","$transitionSuppressDeprecated",function(a,b,c,d,e){function f(a){for(var b in a)if(void 0!==h.style[b])return a[b]}e||d.warn("$transition is now deprecated. Use $animate from ngAnimate instead.");var g=function(d,e,f){f=f||{};var h=a.defer(),i=g[f.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(e)?d.addClass(e):angular.isFunction(e)?e(d):angular.isObject(e)&&d.css(e),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},h=document.createElement("trans"),i={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},j={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return g.transitionEndEventName=f(i),g.animationEndEventName=f(j),g}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$rootScope","$position","typeaheadParser",function(a,b,c,d,e,f,g,h){var i=[9,13,27,38,40];return{require:"ngModel",link:function(j,k,l,m){var n=j.$eval(l.typeaheadMinLength);n||0===n||(n=1);var o,p=j.$eval(l.typeaheadWaitMs)||0,q=j.$eval(l.typeaheadEditable)!==!1,r=b(l.typeaheadLoading).assign||angular.noop,s=b(l.typeaheadOnSelect),t=l.typeaheadInputFormatter?b(l.typeaheadInputFormatter):void 0,u=l.typeaheadAppendToBody?j.$eval(l.typeaheadAppendToBody):!1,v=j.$eval(l.typeaheadFocusFirst)!==!1,w=b(l.ngModel).assign,x=h.parse(l.typeahead),y=j.$new();j.$on("$destroy",function(){y.$destroy()});var z="typeahead-"+y.$id+"-"+Math.floor(1e4*Math.random());k.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":z});var A=angular.element("
        ");A.attr({id:z,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(l.typeaheadTemplateUrl)&&A.attr("template-url",l.typeaheadTemplateUrl);var B=function(){y.matches=[],y.activeIdx=-1,k.attr("aria-expanded",!1)},C=function(a){return z+"-option-"+a};y.$watch("activeIdx",function(a){0>a?k.removeAttr("aria-activedescendant"):k.attr("aria-activedescendant",C(a))});var D=function(a){var b={$viewValue:a};r(j,!0),c.when(x.source(j,b)).then(function(c){var d=a===m.$viewValue;if(d&&o)if(c&&c.length>0){y.activeIdx=v?0:-1,y.matches.length=0;for(var e=0;e=n?p>0?(G(),F(a)):D(a):(r(j,!1),G(),B()),q?a:a?void m.$setValidity("editable",!1):(m.$setValidity("editable",!0),a)}),m.$formatters.push(function(a){var b,c,d={};return q||m.$setValidity("editable",!0),t?(d.$model=a,t(j,d)):(d[x.itemName]=a,b=x.viewMapper(j,d),d[x.itemName]=void 0,c=x.viewMapper(j,d),b!==c?b:a)}),y.select=function(a){var b,c,e={};e[x.itemName]=c=y.matches[a].model,b=x.modelMapper(j,e),w(j,b),m.$setValidity("editable",!0),m.$setValidity("parse",!0),s(j,{$item:c,$model:b,$label:x.viewMapper(j,e)}),B(),d(function(){k[0].focus()},0,!1)},k.bind("keydown",function(a){0!==y.matches.length&&-1!==i.indexOf(a.which)&&(-1!=y.activeIdx||13!==a.which&&9!==a.which)&&(a.preventDefault(),40===a.which?(y.activeIdx=(y.activeIdx+1)%y.matches.length,y.$digest()):38===a.which?(y.activeIdx=(y.activeIdx>0?y.activeIdx:y.matches.length)-1,y.$digest()):13===a.which||9===a.which?y.$apply(function(){y.select(y.activeIdx)}):27===a.which&&(a.stopPropagation(),B(),y.$digest()))}),k.bind("blur",function(){o=!1});var H=function(a){k[0]!==a.target&&(B(),f.$$phase||y.$digest())};e.bind("click",H),j.$on("$destroy",function(){e.unbind("click",H),u&&I.remove(),A.remove()});var I=a(A)(y);u?e.find("body").append(I):k.after(I)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"&",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$templateRequest","$compile","$parse",function(a,b,c){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(d,e,f){var g=c(f.templateUrl)(d.$parent)||"template/typeahead/typeahead-match.html";a(g).then(function(a){b(a.trim())(d,function(a){e.replaceWith(a)})})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}),!angular.$$csp()&&angular.element(document).find("head").prepend(''); \ No newline at end of file + */angular.module("ui.bootstrap",["ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$q","$parse","$injector",function(e,t,n,o){var i=o.has("$animateCss")?o.get("$animateCss"):null;return{link:function(o,a,r){function l(){v=!!("horizontal"in r),v?(b={width:""},$={width:"0"}):(b={height:""},$={height:"0"}),o.$eval(r.uibCollapse)||a.addClass("in").addClass("collapse").attr("aria-expanded",!0).attr("aria-hidden",!1).css(b)}function s(e){return v?{width:e.scrollWidth+"px"}:{height:e.scrollHeight+"px"}}function u(){a.hasClass("collapse")&&a.hasClass("in")||t.resolve(f(o)).then(function(){a.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),i?i(a,{addClass:"in",easing:"ease",css:{overflow:"hidden"},to:s(a[0])}).start()["finally"](p):e.addClass(a,"in",{css:{overflow:"hidden"},to:s(a[0])}).then(p)},angular.noop)}function p(){a.removeClass("collapsing").addClass("collapse").css(b),m(o)}function c(){return a.hasClass("collapse")||a.hasClass("in")?void t.resolve(h(o)).then(function(){a.css(s(a[0])).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),i?i(a,{removeClass:"in",to:$}).start()["finally"](d):e.removeClass(a,"in",{to:$}).then(d)},angular.noop):d()}function d(){a.css($),a.removeClass("collapsing").addClass("collapse"),g(o)}var f=n(r.expanding),m=n(r.expanded),h=n(r.collapsing),g=n(r.collapsed),v=!1,b={},$={};l(),o.$watch(r.uibCollapse,function(e){e?c():u()})}}}]),angular.module("ui.bootstrap.tabindex",[]).directive("uibTabindexToggle",function(){return{restrict:"A",link:function(e,t,n){n.$observe("disabled",function(e){n.$set("tabindex",e?-1:null)})}}}),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse","ui.bootstrap.tabindex"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(e,t,n){this.groups=[],this.closeOthers=function(o){var i=angular.isDefined(t.closeOthers)?e.$eval(t.closeOthers):n.closeOthers;i&&angular.forEach(this.groups,function(e){e!==o&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(n){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(e,t){return t.templateUrl||"uib/template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,restrict:"A",templateUrl:function(e,t){return t.templateUrl||"uib/template/accordion/accordion-group.html"},scope:{heading:"@",panelClass:"@?",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(e,t,n,o){t.addClass("panel"),o.addGroup(e),e.openClass=n.openClass||"panel-open",e.panelClass=n.panelClass||"panel-default",e.$watch("isOpen",function(n){t.toggleClass(e.openClass,!!n),n&&o.closeOthers(e)}),e.toggleOpen=function(t){e.isDisabled||t&&32!==t.which||(e.isOpen=!e.isOpen)};var i="accordiongroup-"+e.$id+"-"+Math.floor(1e4*Math.random());e.headingId=i+"-tab",e.panelId=i+"-panel"}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(e,t,n,o,i){o.setHeading(i(e,angular.noop))}}}).directive("uibAccordionTransclude",function(){function e(){return"uib-accordion-header,data-uib-accordion-header,x-uib-accordion-header,uib\\:accordion-header,[uib-accordion-header],[data-uib-accordion-header],[x-uib-accordion-header]"}return{require:"^uibAccordionGroup",link:function(t,n,o,i){t.$watch(function(){return i[o.uibAccordionTransclude]},function(t){if(t){var o=angular.element(n[0].querySelector(e()));o.html(""),o.append(t)}})}}}),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$element","$attrs","$interpolate","$timeout",function(e,t,n,o,i){e.closeable=!!n.close,t.addClass("alert"),n.$set("role","alert"),e.closeable&&t.addClass("alert-dismissible");var a=angular.isDefined(n.dismissOnTimeout)?o(n.dismissOnTimeout)(e.$parent):null;a&&i(function(){e.close()},parseInt(a,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",restrict:"A",templateUrl:function(e,t){return t.templateUrl||"uib/template/alert/alert.html"},transclude:!0,scope:{close:"&"}}}),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("uibBtnRadio",["$parse",function(e){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(t,n,o,i){var a=i[0],r=i[1],l=e(o.uibUncheckable);n.find("input").css({display:"none"}),r.$render=function(){n.toggleClass(a.activeClass,angular.equals(r.$modelValue,t.$eval(o.uibBtnRadio)))},n.on(a.toggleEvent,function(){if(!o.disabled){var e=n.hasClass(a.activeClass);(!e||angular.isDefined(o.uncheckable))&&t.$apply(function(){r.$setViewValue(e?null:t.$eval(o.uibBtnRadio)),r.$render()})}}),o.uibUncheckable&&t.$watch(l,function(e){o.$set("uncheckable",e?"":void 0)})}}}]).directive("uibBtnCheckbox",function(){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(e,t,n,o){function i(){return r(n.btnCheckboxTrue,!0)}function a(){return r(n.btnCheckboxFalse,!1)}function r(t,n){return angular.isDefined(t)?e.$eval(t):n}var l=o[0],s=o[1];t.find("input").css({display:"none"}),s.$render=function(){t.toggleClass(l.activeClass,angular.equals(s.$modelValue,i()))},t.on(l.toggleEvent,function(){n.disabled||e.$apply(function(){s.$setViewValue(t.hasClass(l.activeClass)?a():i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$timeout","$animate",function(e,t,n,o,i){function a(e){for(var t=0,n=g.length;n>t;++t)if(g[t].index===e)return g[t]}function r(e){for(var t=0;t1){g[o].element.data(v,n.direction);var s=h.getCurrentIndex();angular.isNumber(s)&&g[s].element&&g[s].element.data(v,n.direction),e.$currentTransition=!0,i.on("addClass",g[o].element,function(t,n){"close"===n&&(e.$currentTransition=null,i.off("addClass",t))})}e.active=n.index,b=n.index,r(o),e.$evalAsync(function(){angular.extend(a(h.getCurrentIndex()),{fetch:!0})}),c()}}function s(e){for(var t=0;t0&&(f=n(d,t))}function d(){var t=+e.interval;m&&!isNaN(t)&&t>0&&g.length?e.next():e.pause()}var f,m,h=this,g=h.slides=e.slides=[],v="uib-slideDirection",b=e.active,$=!1;t.addClass("carousel"),h.addSlide=function(t,n){g.push({slide:t,element:n}),g.sort(function(e,t){return+e.slide.index-+t.slide.index}),(t.index===e.active||1===g.length&&!angular.isNumber(e.active))&&(e.$currentTransition&&(e.$currentTransition=null),b=t.index,e.active=t.index,r(b),h.select(g[s(t)]),1===g.length&&e.play())},h.getCurrentIndex=function(){for(var e=0;e0&&b===n?n>=g.length?(b=g.length-1,e.active=b,r(b),h.select(g[g.length-1])):(b=n,e.active=b,r(b),h.select(g[n])):b>n&&(b--,e.active=b),0===g.length&&(b=null,e.active=null)},h.select=e.select=function(t,n){var o=s(t.slide);void 0===n&&(n=o>h.getCurrentIndex()?"next":"prev"),t.slide.index===b||e.$currentTransition||l(t.slide,o,n)},e.indexOfSlide=function(e){return+e.slide.index},e.isActive=function(t){return e.active===t.slide.index},e.isPrevDisabled=function(){return 0===e.active&&e.noWrap()},e.isNextDisabled=function(){return e.active===g.length-1&&e.noWrap()},e.pause=function(){e.noPause||(m=!1,u())},e.play=function(){m||(m=!0,c())},t.on("mouseenter",e.pause),t.on("mouseleave",e.play),e.$on("$destroy",function(){$=!0,u()}),e.$watch("noTransition",function(e){i.enabled(t,!e)}),e.$watch("interval",c),e.$watchCollection("slides",p),e.$watch("active",function(e){if(angular.isNumber(e)&&b!==e){for(var t=0;t-1){var a=!1;e=e.split("");for(var r=i;r-1){e=e.split(""),n[i]="("+o.regex+")",e[i]="$";for(var a=i+1,r=i+o.key.length;r>a;a++)n[a]="",e[a]="$";e=e.join(""),t.push({index:i,key:o.key,apply:o.apply,matcher:o.regex})}}),{regex:new RegExp("^"+n.join("")+"$"),map:o(t,"index")}}function l(e){for(var t,n,o=[],i=0;i=e.length||"'"!==e.charAt(i+1))&&(o.push(s(e,n,i)),n=null);else if(i===e.length)for(;nn?!1:1===t&&n>28?29===n&&(e%4===0&&e%100!==0||e%400===0):3===t||5===t||8===t||10===t?31>n:!0}function c(e){return parseInt(e,10)}function d(e,t){return e&&t?g(e,t):e}function f(e,t){return e&&t?g(e,t,!0):e}function m(e,t){e=e.replace(/:/g,"");var n=Date.parse("Jan 01, 1970 00:00:00 "+e)/6e4;return isNaN(n)?t:n}function h(e,t){return e=new Date(e.getTime()),e.setMinutes(e.getMinutes()+t),e}function g(e,t,n){n=n?-1:1;var o=e.getTimezoneOffset(),i=m(t,o);return h(e,n*(i-o))}var v,b,$=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){v=t.id,this.parsers={},this.formatters={},b=[{key:"yyyy",regex:"\\d{4}",apply:function(e){this.year=+e},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"yyyy")}},{key:"yy",regex:"\\d{2}",apply:function(e){e=+e,this.year=69>e?e+2e3:e+1900},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"yy")}},{key:"y",regex:"\\d{1,4}",apply:function(e){this.year=+e},formatter:function(e){var t=new Date;return t.setFullYear(Math.abs(e.getFullYear())),n(t,"y")}},{key:"M!",regex:"0?[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){var t=e.getMonth();return/^[0-9]$/.test(t)?n(e,"MM"):n(e,"M")}},{key:"MMMM",regex:t.DATETIME_FORMATS.MONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.MONTH.indexOf(e)},formatter:function(e){return n(e,"MMMM")}},{key:"MMM",regex:t.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.SHORTMONTH.indexOf(e)},formatter:function(e){return n(e,"MMM")}},{key:"MM",regex:"0[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){return n(e,"MM")}},{key:"M",regex:"[1-9]|1[0-2]",apply:function(e){this.month=e-1},formatter:function(e){return n(e,"M")}},{key:"d!",regex:"[0-2]?[0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){var t=e.getDate();return/^[1-9]$/.test(t)?n(e,"dd"):n(e,"d")}},{key:"dd",regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){return n(e,"dd")}},{key:"d",regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(e){this.date=+e},formatter:function(e){return n(e,"d")}},{key:"EEEE",regex:t.DATETIME_FORMATS.DAY.join("|"),formatter:function(e){return n(e,"EEEE")}},{key:"EEE",regex:t.DATETIME_FORMATS.SHORTDAY.join("|"),formatter:function(e){return n(e,"EEE")}},{key:"HH",regex:"(?:0|1)[0-9]|2[0-3]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"HH")}},{key:"hh",regex:"0[0-9]|1[0-2]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"hh")}},{key:"H",regex:"1?[0-9]|2[0-3]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"H")}},{key:"h",regex:"[0-9]|1[0-2]",apply:function(e){this.hours=+e},formatter:function(e){return n(e,"h")}},{key:"mm",regex:"[0-5][0-9]",apply:function(e){this.minutes=+e},formatter:function(e){return n(e,"mm")}},{key:"m",regex:"[0-9]|[1-5][0-9]",apply:function(e){this.minutes=+e},formatter:function(e){return n(e,"m")}},{key:"sss",regex:"[0-9][0-9][0-9]",apply:function(e){this.milliseconds=+e},formatter:function(e){return n(e,"sss")}},{key:"ss",regex:"[0-5][0-9]",apply:function(e){this.seconds=+e},formatter:function(e){return n(e,"ss")}},{key:"s",regex:"[0-9]|[1-5][0-9]",apply:function(e){this.seconds=+e},formatter:function(e){return n(e,"s")}},{key:"a",regex:t.DATETIME_FORMATS.AMPMS.join("|"),apply:function(e){12===this.hours&&(this.hours=0),"PM"===e&&(this.hours+=12)},formatter:function(e){return n(e,"a")}},{key:"Z",regex:"[+-]\\d{4}",apply:function(e){var t=e.match(/([+-])(\d{2})(\d{2})/),n=t[1],o=t[2],i=t[3];this.hours+=c(n+o),this.minutes+=c(n+i)},formatter:function(e){return n(e,"Z")}},{key:"ww",regex:"[0-4][0-9]|5[0-3]",formatter:function(e){return n(e,"ww")}},{key:"w",regex:"[0-9]|[1-4][0-9]|5[0-3]",formatter:function(e){return n(e,"w")}},{key:"GGGG",regex:t.DATETIME_FORMATS.ERANAMES.join("|").replace(/\s/g,"\\s"),formatter:function(e){return n(e,"GGGG")}},{key:"GGG",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"GGG")}},{key:"GG",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"GG")}},{key:"G",regex:t.DATETIME_FORMATS.ERAS.join("|"),formatter:function(e){return n(e,"G")}}],angular.version.major>=1&&angular.version.minor>4&&b.push({key:"LLLL",regex:t.DATETIME_FORMATS.STANDALONEMONTH.join("|"),apply:function(e){this.month=t.DATETIME_FORMATS.STANDALONEMONTH.indexOf(e)},formatter:function(e){return n(e,"LLLL")}})},this.init(),this.getParser=function(e){var t=a(e);return t&&t.apply||null},this.overrideParser=function(e,t){var n=a(e);n&&angular.isFunction(t)&&(this.parsers={},n.apply=t)}.bind(this),this.filter=function(e,n){if(!angular.isDate(e)||isNaN(e)||!n)return"";n=t.DATETIME_FORMATS[n]||n,t.id!==v&&this.init(),this.formatters[n]||(this.formatters[n]=l(n));var o=this.formatters[n];return o.reduce(function(t,n){return t+n(e)},"")},this.parse=function(n,o,i){if(!angular.isString(n)||!o)return n;o=t.DATETIME_FORMATS[o]||o,o=o.replace($,"\\$&"),t.id!==v&&this.init(),this.parsers[o]||(this.parsers[o]=r(o,"apply"));var a=this.parsers[o],l=a.regex,s=a.map,u=n.match(l),c=!1;if(u&&u.length){var d,f;angular.isDate(i)&&!isNaN(i.getTime())?d={year:i.getFullYear(),month:i.getMonth(),date:i.getDate(),hours:i.getHours(),minutes:i.getMinutes(),seconds:i.getSeconds(),milliseconds:i.getMilliseconds()}:(i&&e.warn("dateparser:","baseDate is not a valid date"),d={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var m=1,h=u.length;h>m;m++){var g=s[m-1];"Z"===g.matcher&&(c=!0),g.apply&&g.apply.call(d,u[m])}var b=c?Date.prototype.setUTCFullYear:Date.prototype.setFullYear,w=c?Date.prototype.setUTCHours:Date.prototype.setHours;return p(d.year,d.month,d.date)&&(!angular.isDate(i)||isNaN(i.getTime())||c?(f=new Date(0),b.call(f,d.year,d.month,d.date),w.call(f,d.hours||0,d.minutes||0,d.seconds||0,d.milliseconds||0)):(f=new Date(i),b.call(f,d.year,d.month,d.date),w.call(f,d.hours,d.minutes,d.seconds,d.milliseconds))),f}},this.toTimezone=d,this.fromTimezone=f,this.timezoneToOffset=m,this.addDateMinutes=h,this.convertTimezoneToLocal=g}]),angular.module("ui.bootstrap.isClass",[]).directive("uibIsClass",["$animate",function(e){var t=/^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/,n=/^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;return{restrict:"A",compile:function(o,i){function a(e,t,n){s.push(e),u.push({scope:e,element:t}),m.forEach(function(t,n){r(t,e)}),e.$on("$destroy",l)}function r(t,o){var i=t.match(n),a=o.$eval(i[1]),r=i[2],l=p[t];if(!l){var s=function(t){var n=null;u.some(function(e){var o=e.scope.$eval(d);return o===t?(n=e,!0):void 0}),l.lastActivated!==n&&(l.lastActivated&&e.removeClass(l.lastActivated.element,a),n&&e.addClass(n.element,a),l.lastActivated=n)};p[t]=l={lastActivated:null,scope:o,watchFn:s,compareWithExp:r,watcher:o.$watch(r,s)}}l.watchFn(o.$eval(r))}function l(e){var t=e.targetScope,n=s.indexOf(t);if(s.splice(n,1),u.splice(n,1),s.length){var o=s[0];angular.forEach(p,function(e){e.scope===t&&(e.watcher=o.$watch(e.compareWithExp,e.watchFn),e.scope=o)})}else p={}}var s=[],u=[],p={},c=i.uibIsClass.match(t),d=c[2],f=c[1],m=f.split(",");return a}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.isClass"]).value("$datepickerSuppressError",!1).value("$datepickerLiteralWarning",!0).constant("uibDatepickerConfig",{datepickerMode:"day",formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",maxDate:null,maxMode:"year",minDate:null,minMode:"day",monthColumns:3,ngModelOptions:{},shortcutPropagation:!1,showWeeks:!0,yearColumns:5,yearRows:4}).controller("UibDatepickerController",["$scope","$element","$attrs","$parse","$interpolate","$locale","$log","dateFilter","uibDatepickerConfig","$datepickerLiteralWarning","$datepickerSuppressError","uibDateParser",function(e,t,n,o,i,a,r,l,s,u,p,c){function d(t){e.datepickerMode=t,e.datepickerOptions.datepickerMode=t}function f(t){var n;if(angular.version.minor<6)n=t.$options||e.datepickerOptions.ngModelOptions||s.ngModelOptions||{},n.getOption=function(e){return n[e]};else{var o=t.$options.getOption("timezone")||(e.datepickerOptions.ngModelOptions?e.datepickerOptions.ngModelOptions.timezone:null)||(s.ngModelOptions?s.ngModelOptions.timezone:null);n=t.$options.createChild(s.ngModelOptions).createChild(e.datepickerOptions.ngModelOptions).createChild(t.$options).createChild({timezone:o})}return n}var m=this,h={$setViewValue:angular.noop},g={},v=[];t.addClass("uib-datepicker"),n.$set("role","application"),e.datepickerOptions||(e.datepickerOptions={}),this.modes=["day","month","year"],["customClass","dateDisabled","datepickerMode","formatDay","formatDayHeader","formatDayTitle","formatMonth","formatMonthTitle","formatYear","maxDate","maxMode","minDate","minMode","monthColumns","showWeeks","shortcutPropagation","startingDay","yearColumns","yearRows"].forEach(function(t){switch(t){case"customClass":case"dateDisabled":e[t]=e.datepickerOptions[t]||angular.noop;break;case"datepickerMode":e.datepickerMode=angular.isDefined(e.datepickerOptions.datepickerMode)?e.datepickerOptions.datepickerMode:s.datepickerMode;break;case"formatDay":case"formatDayHeader":case"formatDayTitle":case"formatMonth":case"formatMonthTitle":case"formatYear":m[t]=angular.isDefined(e.datepickerOptions[t])?i(e.datepickerOptions[t])(e.$parent):s[t];break;case"monthColumns":case"showWeeks":case"shortcutPropagation":case"yearColumns":case"yearRows":m[t]=angular.isDefined(e.datepickerOptions[t])?e.datepickerOptions[t]:s[t];break;case"startingDay":angular.isDefined(e.datepickerOptions.startingDay)?m.startingDay=e.datepickerOptions.startingDay:angular.isNumber(s.startingDay)?m.startingDay=s.startingDay:m.startingDay=(a.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7;break;case"maxDate":case"minDate":e.$watch("datepickerOptions."+t,function(e){e?angular.isDate(e)?m[t]=c.fromTimezone(new Date(e),g.getOption("timezone")):(u&&r.warn("Literal date support has been deprecated, please switch to date object usage"),m[t]=new Date(l(e,"medium"))):m[t]=s[t]?c.fromTimezone(new Date(s[t]),g.getOption("timezone")):null,m.refreshView()});break;case"maxMode":case"minMode":e.datepickerOptions[t]?e.$watch(function(){return e.datepickerOptions[t]},function(n){m[t]=e[t]=angular.isDefined(n)?n:e.datepickerOptions[t],("minMode"===t&&m.modes.indexOf(e.datepickerOptions.datepickerMode)m.modes.indexOf(m[t]))&&(e.datepickerMode=m[t],e.datepickerOptions.datepickerMode=m[t])}):m[t]=e[t]=s[t]||null}}),e.uniqueId="datepicker-"+e.$id+"-"+Math.floor(1e4*Math.random()),e.disabled=angular.isDefined(n.disabled)||!1,angular.isDefined(n.ngDisabled)&&v.push(e.$parent.$watch(n.ngDisabled,function(t){e.disabled=t,m.refreshView()})),e.isActive=function(t){return 0===m.compare(t.date,m.activeDate)?(e.activeDateId=t.uid,!0):!1},this.init=function(t){h=t,g=f(h),e.datepickerOptions.initDate?(m.activeDate=c.fromTimezone(e.datepickerOptions.initDate,g.getOption("timezone"))||new Date,e.$watch("datepickerOptions.initDate",function(e){e&&(h.$isEmpty(h.$modelValue)||h.$invalid)&&(m.activeDate=c.fromTimezone(e,g.getOption("timezone")),m.refreshView())})):m.activeDate=new Date;var n=h.$modelValue?new Date(h.$modelValue):new Date;this.activeDate=isNaN(n)?c.fromTimezone(new Date,g.getOption("timezone")):c.fromTimezone(n,g.getOption("timezone")),h.$render=function(){m.render()}},this.render=function(){if(h.$viewValue){var e=new Date(h.$viewValue),t=!isNaN(e);t?this.activeDate=c.fromTimezone(e,g.getOption("timezone")):p||r.error('Datepicker directive: "ng-model" value must be a Date object')}this.refreshView()},this.refreshView=function(){if(this.element){e.selectedDt=null,this._refreshView(),e.activeDt&&(e.activeDateId=e.activeDt.uid);var t=h.$viewValue?new Date(h.$viewValue):null;t=c.fromTimezone(t,g.getOption("timezone")),h.$setValidity("dateDisabled",!t||this.element&&!this.isDisabled(t))}},this.createDateObject=function(t,n){var o=h.$viewValue?new Date(h.$viewValue):null;o=c.fromTimezone(o,g.getOption("timezone"));var i=new Date;i=c.fromTimezone(i,g.getOption("timezone"));var a=this.compare(t,i),r={date:t,label:c.filter(t,n),selected:o&&0===this.compare(t,o),disabled:this.isDisabled(t),past:0>a,current:0===a,future:a>0,customClass:this.customClass(t)||null};return o&&0===this.compare(t,o)&&(e.selectedDt=r),m.activeDate&&0===this.compare(r.date,m.activeDate)&&(e.activeDt=r),r},this.isDisabled=function(t){return e.disabled||this.minDate&&this.compare(t,this.minDate)<0||this.maxDate&&this.compare(t,this.maxDate)>0||e.dateDisabled&&e.dateDisabled({date:t,mode:e.datepickerMode})},this.customClass=function(t){return e.customClass({date:t,mode:e.datepickerMode})},this.split=function(e,t){for(var n=[];e.length>0;)n.push(e.splice(0,t));return n},e.select=function(t){if(e.datepickerMode===m.minMode){var n=h.$viewValue?c.fromTimezone(new Date(h.$viewValue),g.getOption("timezone")):new Date(0,0,0,0,0,0,0);n.setFullYear(t.getFullYear(),t.getMonth(),t.getDate()),n=c.toTimezone(n,g.getOption("timezone")),h.$setViewValue(n),h.$render()}else m.activeDate=t,d(m.modes[m.modes.indexOf(e.datepickerMode)-1]),e.$emit("uib:datepicker.mode");e.$broadcast("uib:datepicker.focus")},e.move=function(e){var t=m.activeDate.getFullYear()+e*(m.step.years||0),n=m.activeDate.getMonth()+e*(m.step.months||0);m.activeDate.setFullYear(t,n,1),m.refreshView()},e.toggleMode=function(t){t=t||1,e.datepickerMode===m.maxMode&&1===t||e.datepickerMode===m.minMode&&-1===t||(d(m.modes[m.modes.indexOf(e.datepickerMode)+t]),e.$emit("uib:datepicker.mode"))},e.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var b=function(){m.element[0].focus()};e.$on("uib:datepicker.focus",b),e.keydown=function(t){var n=e.keys[t.which];if(n&&!t.shiftKey&&!t.altKey&&!e.disabled)if(t.preventDefault(),m.shortcutPropagation||t.stopPropagation(),"enter"===n||"space"===n){if(m.isDisabled(m.activeDate))return;e.select(m.activeDate)}else!t.ctrlKey||"up"!==n&&"down"!==n?(m.handleKeyDown(n,t),m.refreshView()):e.toggleMode("up"===n?1:-1)},t.on("keydown",function(t){e.$apply(function(){e.keydown(t)})}),e.$on("$destroy",function(){for(;v.length;)v.shift()()})}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(e,t,n){function o(e,t){return 1!==t||e%4!==0||e%100===0&&e%400!==0?a[t]:29}function i(e){var t=new Date(e);t.setDate(t.getDate()+4-(t.getDay()||7));var n=t.getTime();return t.setMonth(0),t.setDate(1),Math.floor(Math.round((n-t)/864e5)/7)+1}var a=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=t,this.init=function(t){angular.extend(t,this),e.showWeeks=t.showWeeks,t.refreshView()},this.getDates=function(e,t){for(var n,o=new Array(t),i=new Date(e),a=0;t>a;)n=new Date(i),o[a++]=n,i.setDate(i.getDate()+1);return o},this._refreshView=function(){var t=this.activeDate.getFullYear(),o=this.activeDate.getMonth(),a=new Date(this.activeDate);a.setFullYear(t,o,1);var r=this.startingDay-a.getDay(),l=r>0?7-r:-r,s=new Date(a);l>0&&s.setDate(-l+1);for(var u=this.getDates(s,42),p=0;42>p;p++)u[p]=angular.extend(this.createDateObject(u[p],this.formatDay),{secondary:u[p].getMonth()!==o,uid:e.uniqueId+"-"+p});e.labels=new Array(7);for(var c=0;7>c;c++)e.labels[c]={abbr:n(u[c].date,this.formatDayHeader),full:n(u[c].date,"EEEE")};if(e.title=n(this.activeDate,this.formatDayTitle),e.rows=this.split(u,7),e.showWeeks){e.weekNumbers=[];for(var d=(11-this.startingDay)%7,f=e.rows.length,m=0;f>m;m++)e.weekNumbers.push(i(e.rows[m][d].date))}},this.compare=function(e,t){var n=new Date(e.getFullYear(),e.getMonth(),e.getDate()),o=new Date(t.getFullYear(),t.getMonth(),t.getDate());return n.setFullYear(e.getFullYear()),o.setFullYear(t.getFullYear()),n-o},this.handleKeyDown=function(e,t){var n=this.activeDate.getDate();if("left"===e)n-=1;else if("up"===e)n-=7;else if("right"===e)n+=1;else if("down"===e)n+=7;else if("pageup"===e||"pagedown"===e){var i=this.activeDate.getMonth()+("pageup"===e?-1:1);this.activeDate.setMonth(i,1),n=Math.min(o(this.activeDate.getFullYear(),this.activeDate.getMonth()),n)}else"home"===e?n=1:"end"===e&&(n=o(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(n)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(e,t,n){this.step={years:1},this.element=t,this.init=function(e){angular.extend(e,this),e.refreshView()},this._refreshView=function(){for(var t,o=new Array(12),i=this.activeDate.getFullYear(),a=0;12>a;a++)t=new Date(this.activeDate),t.setFullYear(i,a,1),o[a]=angular.extend(this.createDateObject(t,this.formatMonth),{uid:e.uniqueId+"-"+a});e.title=n(this.activeDate,this.formatMonthTitle),e.rows=this.split(o,this.monthColumns),e.yearHeaderColspan=this.monthColumns>3?this.monthColumns-2:1},this.compare=function(e,t){var n=new Date(e.getFullYear(),e.getMonth()),o=new Date(t.getFullYear(),t.getMonth());return n.setFullYear(e.getFullYear()),o.setFullYear(t.getFullYear()),n-o},this.handleKeyDown=function(e,t){var n=this.activeDate.getMonth();if("left"===e)n-=1;else if("up"===e)n-=this.monthColumns;else if("right"===e)n+=1;else if("down"===e)n+=this.monthColumns;else if("pageup"===e||"pagedown"===e){var o=this.activeDate.getFullYear()+("pageup"===e?-1:1);this.activeDate.setFullYear(o)}else"home"===e?n=0:"end"===e&&(n=11);this.activeDate.setMonth(n)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(e,t,n){function o(e){return parseInt((e-1)/a,10)*a+1}var i,a;this.element=t,this.yearpickerInit=function(){i=this.yearColumns,a=this.yearRows*i,this.step={years:a}},this._refreshView=function(){for(var t,n=new Array(a),r=0,l=o(this.activeDate.getFullYear());a>r;r++)t=new Date(this.activeDate),t.setFullYear(l+r,0,1),n[r]=angular.extend(this.createDateObject(t,this.formatYear),{uid:e.uniqueId+"-"+r});e.title=[n[0].label,n[a-1].label].join(" - "),e.rows=this.split(n,i),e.columns=i},this.compare=function(e,t){return e.getFullYear()-t.getFullYear()},this.handleKeyDown=function(e,t){var n=this.activeDate.getFullYear();"left"===e?n-=1:"up"===e?n-=i:"right"===e?n+=1:"down"===e?n+=i:"pageup"===e||"pagedown"===e?n+=("pageup"===e?-1:1)*a:"home"===e?n=o(this.activeDate.getFullYear()):"end"===e&&(n=o(this.activeDate.getFullYear())+a-1),this.activeDate.setFullYear(n)}}]).directive("uibDatepicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/datepicker.html"},scope:{datepickerOptions:"=?"},require:["uibDatepicker","^ngModel"],restrict:"A",controller:"UibDatepickerController",controllerAs:"datepicker",link:function(e,t,n,o){var i=o[0],a=o[1];i.init(a)}}}).directive("uibDaypicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/day.html"},require:["^uibDatepicker","uibDaypicker"],restrict:"A",controller:"UibDaypickerController",link:function(e,t,n,o){var i=o[0],a=o[1];a.init(i)}}}).directive("uibMonthpicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/month.html"},require:["^uibDatepicker","uibMonthpicker"],restrict:"A",controller:"UibMonthpickerController",link:function(e,t,n,o){var i=o[0],a=o[1];a.init(i)}}}).directive("uibYearpicker",function(){return{templateUrl:function(e,t){return t.templateUrl||"uib/template/datepicker/year.html"},require:["^uibDatepicker","uibYearpicker"],restrict:"A",controller:"UibYearpickerController",link:function(e,t,n,o){var i=o[0];angular.extend(i,o[1]),i.yearpickerInit(),i.refreshView()}}}),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(e,t){var n,o,i={normal:/(auto|scroll)/,hidden:/(auto|scroll|hidden)/},a={auto:/\s?auto?\s?/i,primary:/^(top|bottom|left|right)$/,secondary:/^(top|bottom|left|right|center)$/, + vertical:/^(top|bottom)$/},r=/(HTML|BODY)/;return{getRawNode:function(e){return e.nodeName?e:e[0]||e},parseStyle:function(e){return e=parseFloat(e),isFinite(e)?e:0},offsetParent:function(n){function o(e){return"static"===(t.getComputedStyle(e).position||"static")}n=this.getRawNode(n);for(var i=n.offsetParent||e[0].documentElement;i&&i!==e[0].documentElement&&o(i);)i=i.offsetParent;return i||e[0].documentElement},scrollbarWidth:function(i){if(i){if(angular.isUndefined(o)){var a=e.find("body");a.addClass("uib-position-body-scrollbar-measure"),o=t.innerWidth-a[0].clientWidth,o=isFinite(o)?o:0,a.removeClass("uib-position-body-scrollbar-measure")}return o}if(angular.isUndefined(n)){var r=angular.element('
        ');e.find("body").append(r),n=r[0].offsetWidth-r[0].clientWidth,n=isFinite(n)?n:0,r.remove()}return n},scrollbarPadding:function(e){e=this.getRawNode(e);var n=t.getComputedStyle(e),o=this.parseStyle(n.paddingRight),i=this.parseStyle(n.paddingBottom),a=this.scrollParent(e,!1,!0),l=this.scrollbarWidth(r.test(a.tagName));return{scrollbarWidth:l,widthOverflow:a.scrollWidth>a.clientWidth,right:o+l,originalRight:o,heightOverflow:a.scrollHeight>a.clientHeight,bottom:i+l,originalBottom:i}},isScrollable:function(e,n){e=this.getRawNode(e);var o=n?i.hidden:i.normal,a=t.getComputedStyle(e);return o.test(a.overflow+a.overflowY+a.overflowX)},scrollParent:function(n,o,a){n=this.getRawNode(n);var r=o?i.hidden:i.normal,l=e[0].documentElement,s=t.getComputedStyle(n);if(a&&r.test(s.overflow+s.overflowY+s.overflowX))return n;var u="absolute"===s.position,p=n.parentElement||l;if(p===l||"fixed"===s.position)return l;for(;p.parentElement&&p!==l;){var c=t.getComputedStyle(p);if(u&&"static"!==c.position&&(u=!1),!u&&r.test(c.overflow+c.overflowY+c.overflowX))break;p=p.parentElement}return p},position:function(n,o){n=this.getRawNode(n);var i=this.offset(n);if(o){var a=t.getComputedStyle(n);i.top-=this.parseStyle(a.marginTop),i.left-=this.parseStyle(a.marginLeft)}var r=this.offsetParent(n),l={top:0,left:0};return r!==e[0].documentElement&&(l=this.offset(r),l.top+=r.clientTop-r.scrollTop,l.left+=r.clientLeft-r.scrollLeft),{width:Math.round(angular.isNumber(i.width)?i.width:n.offsetWidth),height:Math.round(angular.isNumber(i.height)?i.height:n.offsetHeight),top:Math.round(i.top-l.top),left:Math.round(i.left-l.left)}},offset:function(n){n=this.getRawNode(n);var o=n.getBoundingClientRect();return{width:Math.round(angular.isNumber(o.width)?o.width:n.offsetWidth),height:Math.round(angular.isNumber(o.height)?o.height:n.offsetHeight),top:Math.round(o.top+(t.pageYOffset||e[0].documentElement.scrollTop)),left:Math.round(o.left+(t.pageXOffset||e[0].documentElement.scrollLeft))}},viewportOffset:function(n,o,i){n=this.getRawNode(n),i=i!==!1?!0:!1;var a=n.getBoundingClientRect(),r={top:0,left:0,bottom:0,right:0},l=o?e[0].documentElement:this.scrollParent(n),s=l.getBoundingClientRect();if(r.top=s.top+l.clientTop,r.left=s.left+l.clientLeft,l===e[0].documentElement&&(r.top+=t.pageYOffset,r.left+=t.pageXOffset),r.bottom=r.top+l.clientHeight,r.right=r.left+l.clientWidth,i){var u=t.getComputedStyle(l);r.top+=this.parseStyle(u.paddingTop),r.bottom-=this.parseStyle(u.paddingBottom),r.left+=this.parseStyle(u.paddingLeft),r.right-=this.parseStyle(u.paddingRight)}return{top:Math.round(a.top-r.top),bottom:Math.round(r.bottom-a.bottom),left:Math.round(a.left-r.left),right:Math.round(r.right-a.right)}},parsePlacement:function(e){var t=a.auto.test(e);return t&&(e=e.replace(a.auto,"")),e=e.split("-"),e[0]=e[0]||"top",a.primary.test(e[0])||(e[0]="top"),e[1]=e[1]||"center",a.secondary.test(e[1])||(e[1]="center"),t?e[2]=!0:e[2]=!1,e},positionElements:function(e,n,o,i){e=this.getRawNode(e),n=this.getRawNode(n);var r=angular.isDefined(n.offsetWidth)?n.offsetWidth:n.prop("offsetWidth"),l=angular.isDefined(n.offsetHeight)?n.offsetHeight:n.prop("offsetHeight");o=this.parsePlacement(o);var s=i?this.offset(e):this.position(e),u={top:0,left:0,placement:""};if(o[2]){var p=this.viewportOffset(e,i),c=t.getComputedStyle(n),d={width:r+Math.round(Math.abs(this.parseStyle(c.marginLeft)+this.parseStyle(c.marginRight))),height:l+Math.round(Math.abs(this.parseStyle(c.marginTop)+this.parseStyle(c.marginBottom)))};if(o[0]="top"===o[0]&&d.height>p.top&&d.height<=p.bottom?"bottom":"bottom"===o[0]&&d.height>p.bottom&&d.height<=p.top?"top":"left"===o[0]&&d.width>p.left&&d.width<=p.right?"right":"right"===o[0]&&d.width>p.right&&d.width<=p.left?"left":o[0],o[1]="top"===o[1]&&d.height-s.height>p.bottom&&d.height-s.height<=p.top?"bottom":"bottom"===o[1]&&d.height-s.height>p.top&&d.height-s.height<=p.bottom?"top":"left"===o[1]&&d.width-s.width>p.right&&d.width-s.width<=p.left?"right":"right"===o[1]&&d.width-s.width>p.left&&d.width-s.width<=p.right?"left":o[1],"center"===o[1])if(a.vertical.test(o[0])){var f=s.width/2-r/2;p.left+f<0&&d.width-s.width<=p.right?o[1]="left":p.right+f<0&&d.width-s.width<=p.left&&(o[1]="right")}else{var m=s.height/2-d.height/2;p.top+m<0&&d.height-s.height<=p.bottom?o[1]="top":p.bottom+m<0&&d.height-s.height<=p.top&&(o[1]="bottom")}}switch(o[0]){case"top":u.top=s.top-l;break;case"bottom":u.top=s.top+s.height;break;case"left":u.left=s.left-r;break;case"right":u.left=s.left+s.width}switch(o[1]){case"top":u.top=s.top;break;case"bottom":u.top=s.top+s.height-l;break;case"left":u.left=s.left;break;case"right":u.left=s.left+s.width-r;break;case"center":a.vertical.test(o[0])?u.left=s.left+s.width/2-r/2:u.top=s.top+s.height/2-l/2}return u.top=Math.round(u.top),u.left=Math.round(u.left),u.placement="center"===o[1]?o[0]:o[0]+"-"+o[1],u},adjustTop:function(e,t,n,o){return-1!==e.indexOf("top")&&n!==o?{top:t.top-o+"px"}:void 0},positionArrow:function(e,n){e=this.getRawNode(e);var o=e.querySelector(".tooltip-inner, .popover-inner");if(o){var i=angular.element(o).hasClass("tooltip-inner"),r=i?e.querySelector(".tooltip-arrow"):e.querySelector(".arrow");if(r){var l={top:"",bottom:"",left:"",right:""};if(n=this.parsePlacement(n),"center"===n[1])return void angular.element(r).css(l);var s="border-"+n[0]+"-width",u=t.getComputedStyle(r)[s],p="border-";p+=a.vertical.test(n[0])?n[0]+"-"+n[1]:n[1]+"-"+n[0],p+="-radius";var c=t.getComputedStyle(i?o:e)[p];switch(n[0]){case"top":l.bottom=i?"0":"-"+u;break;case"bottom":l.top=i?"0":"-"+u;break;case"left":l.right=i?"0":"-"+u;break;case"right":l.left=i?"0":"-"+u}l[n[1]]=c,angular.element(r).css(l)}}}}}]),angular.module("ui.bootstrap.datepickerPopup",["ui.bootstrap.datepicker","ui.bootstrap.position"]).value("$datepickerPopupLiteralWarning",!0).constant("uibDatepickerPopupConfig",{altInputFormats:[],appendToBody:!1,clearText:"Clear",closeOnDateSelection:!0,closeText:"Done",currentText:"Today",datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"uib/template/datepickerPopup/popup.html",datepickerTemplateUrl:"uib/template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},onOpenFocus:!0,showButtonBar:!0,placement:"auto bottom-left"}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$log","$parse","$window","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout","uibDatepickerConfig","$datepickerPopupLiteralWarning",function(e,t,n,o,i,a,r,l,s,u,p,c,d,f,m,h){function g(t){var n=c.parse(t,k,e.date);if(isNaN(n))for(var o=0;o
        "),E.attr({"ng-model":"date","ng-change":"dateSelection(date)","template-url":M}),P=angular.element(E.children()[0]),P.attr("template-url",O),e.datepickerOptions||(e.datepickerOptions={}),F&&"month"===n.type&&(e.datepickerOptions.datepickerMode="month",e.datepickerOptions.minMode="month"),P.attr("datepicker-options","datepickerOptions"),F?A.$formatters.push(function(t){return e.date=c.fromTimezone(t,I.getOption("timezone")),t}):(A.$$parserName="date",A.$validators.date=b,A.$parsers.unshift(v),A.$formatters.push(function(t){return A.$isEmpty(t)?(e.date=t,t):(angular.isNumber(t)&&(t=new Date(t)),e.date=c.fromTimezone(t,I.getOption("timezone")),c.filter(e.date,k))})),A.$viewChangeListeners.push(function(){e.date=g(A.$viewValue)}),t.on("keydown",w),U=o(E)(e),E.remove(),T?l.find("body").append(U):t.after(U),e.$on("$destroy",function(){for(e.isOpen===!0&&(s.$$phase||e.$apply(function(){e.isOpen=!1})),U.remove(),t.off("keydown",w),l.off("click",$),S&&S.off("scroll",y),angular.element(r).off("resize",y);H.length;)H.shift()()})},e.getText=function(t){return e[t+"Text"]||d[t+"Text"]},e.isDisabled=function(t){"today"===t&&(t=c.fromTimezone(new Date,I.getOption("timezone")));var n={};return angular.forEach(["minDate","maxDate"],function(t){e.datepickerOptions[t]?angular.isDate(e.datepickerOptions[t])?n[t]=new Date(e.datepickerOptions[t]):(h&&i.warn("Literal date support has been deprecated, please switch to date object usage"),n[t]=new Date(p(e.datepickerOptions[t],"medium"))):n[t]=null}),e.datepickerOptions&&n.minDate&&e.compare(t,n.minDate)<0||n.maxDate&&e.compare(t,n.maxDate)>0},e.compare=function(e,t){return new Date(e.getFullYear(),e.getMonth(),e.getDate())-new Date(t.getFullYear(),t.getMonth(),t.getDate())},e.dateSelection=function(n){e.date=n;var o=e.date?c.filter(e.date,k):null;t.val(o),A.$setViewValue(o),C&&(e.isOpen=!1,t[0].focus())},e.keydown=function(n){27===n.which&&(n.stopPropagation(),e.isOpen=!1,t[0].focus())},e.select=function(t,n){if(n.stopPropagation(),"today"===t){var o=new Date;angular.isDate(e.date)?(t=new Date(e.date),t.setFullYear(o.getFullYear(),o.getMonth(),o.getDate())):(t=c.fromTimezone(o,I.getOption("timezone")),t.setHours(0,0,0,0))}e.dateSelection(t)},e.close=function(n){n.stopPropagation(),e.isOpen=!1,t[0].focus()},e.disabled=angular.isDefined(n.disabled)||!1,n.ngDisabled&&H.push(e.$parent.$watch(a(n.ngDisabled),function(t){e.disabled=t})),e.$watch("isOpen",function(o){o?e.disabled?e.isOpen=!1:f(function(){y(),x&&e.$broadcast("uib:datepicker.focus"),l.on("click",$);var o=n.popupPlacement?n.popupPlacement:d.placement;T||u.parsePlacement(o)[2]?(S=S||angular.element(u.scrollParent(t)),S&&S.on("scroll",y)):S=null,angular.element(r).on("resize",y)},0,!1):(l.off("click",$),S&&S.off("scroll",y),angular.element(r).off("resize",y))}),e.$on("uib:datepicker.mode",function(){f(y,0,!1)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{datepickerOptions:"=?",isOpen:"=?",currentText:"@",clearText:"@",closeText:"@"},link:function(e,t,n,o){var i=o[0],a=o[1];a.init(i)}}}).directive("uibDatepickerPopupWrap",function(){return{restrict:"A",transclude:!0,templateUrl:function(e,t){return t.templateUrl||"uib/template/datepickerPopup/popup.html"}}}),angular.module("ui.bootstrap.debounce",[]).factory("$$debounce",["$timeout",function(e){return function(t,n){var o;return function(){var i=this,a=Array.prototype.slice.call(arguments);o&&e.cancel(o),o=e(function(){t.apply(i,a)},n)}}}]),angular.module("ui.bootstrap.multiMap",[]).factory("$$multiMap",function(){return{createNew:function(){var e={};return{entries:function(){return Object.keys(e).map(function(t){return{key:t,value:e[t]}})},get:function(t){return e[t]},hasKey:function(t){return!!e[t]},keys:function(){return Object.keys(e)},put:function(t,n){e[t]||(e[t]=[]),e[t].push(n)},remove:function(t,n){var o=e[t];if(o){var i=o.indexOf(n);-1!==i&&o.splice(i,1),o.length||delete e[t]}}}}}}),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.multiMap","ui.bootstrap.position"]).constant("uibDropdownConfig",{appendToOpenClass:"uib-dropdown-open",openClass:"open"}).service("uibDropdownService",["$document","$rootScope","$$multiMap",function(e,t,n){var o=null,i=n.createNew();this.isOnlyOpen=function(e,t){var n=i.get(t);if(n){var o=n.reduce(function(t,n){return n.scope===e?n:t},{});if(o)return 1===n.length}return!1},this.open=function(t,n,r){if(o||e.on("click",a),o&&o!==t&&(o.isOpen=!1),o=t,r){var l=i.get(r);if(l){var s=l.map(function(e){return e.scope});-1===s.indexOf(t)&&i.put(r,{scope:t})}else i.put(r,{scope:t})}},this.close=function(t,n,r){if(o===t&&(e.off("click",a),e.off("keydown",this.keybindFilter),o=null),r){var l=i.get(r);if(l){var s=l.reduce(function(e,n){return n.scope===t?n:e},{});s&&i.remove(r,s)}}};var a=function(e){if(o&&o.isOpen&&!(e&&"disabled"===o.getAutoClose()||e&&3===e.which)){var n=o.getToggleElement();if(!(e&&n&&n[0].contains(e.target))){var i=o.getDropdownElement();e&&"outsideClick"===o.getAutoClose()&&i&&i[0].contains(e.target)||(o.focusToggleElement(),o.isOpen=!1,t.$$phase||o.$apply())}}};this.keybindFilter=function(e){if(o){var t=o.getDropdownElement(),n=o.getToggleElement(),i=t&&t[0].contains(e.target),r=n&&n[0].contains(e.target);27===e.which?(e.stopPropagation(),o.focusToggleElement(),a()):o.isKeynavEnabled()&&-1!==[38,40].indexOf(e.which)&&o.isOpen&&(i||r)&&(e.preventDefault(),e.stopPropagation(),o.focusDropdownEntry(e.which))}}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(e,t,n,o,i,a,r,l,s,u,p){function c(){t.append(m.dropdownMenu)}var d,f,m=this,h=e.$new(),g=i.appendToOpenClass,v=i.openClass,b=angular.noop,$=n.onToggle?o(n.onToggle):angular.noop,w=!1,y=s.find("body");t.addClass("dropdown"),this.init=function(){n.isOpen&&(f=o(n.isOpen),b=f.assign,e.$watch(f,function(e){h.isOpen=!!e})),w=angular.isDefined(n.keyboardNav)},this.toggle=function(e){return h.isOpen=arguments.length?!!e:!h.isOpen,angular.isFunction(b)&&b(h,h.isOpen),h.isOpen},this.isOpen=function(){return h.isOpen},h.getToggleElement=function(){return m.toggleElement},h.getAutoClose=function(){return n.autoClose||"always"},h.getElement=function(){return t},h.isKeynavEnabled=function(){return w},h.focusDropdownEntry=function(e){var n=m.dropdownMenu?angular.element(m.dropdownMenu).find("a"):t.find("ul").eq(0).find("a");switch(e){case 40:angular.isNumber(m.selectedOption)?m.selectedOption=m.selectedOption===n.length-1?m.selectedOption:m.selectedOption+1:m.selectedOption=0;break;case 38:angular.isNumber(m.selectedOption)?m.selectedOption=0===m.selectedOption?0:m.selectedOption-1:m.selectedOption=n.length-1}n[m.selectedOption].focus()},h.getDropdownElement=function(){return m.dropdownMenu},h.focusToggleElement=function(){m.toggleElement&&m.toggleElement[0].focus()},h.$watch("isOpen",function(i,f){var w=null,D=!1;if(angular.isDefined(n.dropdownAppendTo)){var k=o(n.dropdownAppendTo)(h);k&&(w=angular.element(k))}if(angular.isDefined(n.dropdownAppendToBody)){var C=o(n.dropdownAppendToBody)(h);C!==!1&&(D=!0)}if(D&&!w&&(w=y),w&&m.dropdownMenu&&(i?(w.append(m.dropdownMenu),t.on("$destroy",c)):(t.off("$destroy",c),c())),w&&m.dropdownMenu){var T,x,M,O=l.positionElements(t,m.dropdownMenu,"bottom-left",!0),E=0;if(T={top:O.top+"px",display:i?"block":"none"},x=m.dropdownMenu.hasClass("dropdown-menu-right"),x?(T.left="auto",M=l.scrollbarPadding(w),M.heightOverflow&&M.scrollbarWidth&&(E=M.scrollbarWidth),T.right=window.innerWidth-E-(O.left+t.prop("offsetWidth"))+"px"):(T.left=O.left+"px",T.right="auto"),!D){var P=l.offset(w);T.top=O.top-P.top+"px",x?T.right=window.innerWidth-(O.left-P.left+t.prop("offsetWidth"))+"px":T.left=O.left-P.left+"px"}m.dropdownMenu.css(T)}var S=w?w:t,A=w?g:v,I=S.hasClass(A),U=a.isOnlyOpen(e,w);if(I===!i){var N;N=w?U?"removeClass":"addClass":i?"addClass":"removeClass",r[N](S,A).then(function(){angular.isDefined(i)&&i!==f&&$(e,{open:!!i})})}if(i)m.dropdownMenuTemplateUrl?p(m.dropdownMenuTemplateUrl).then(function(e){d=h.$new(),u(e.trim())(d,function(e){var t=e;m.dropdownMenu.replaceWith(t),m.dropdownMenu=t,s.on("keydown",a.keybindFilter)})}):s.on("keydown",a.keybindFilter),h.focusToggleElement(),a.open(h,t,w);else{if(a.close(h,t,w),m.dropdownMenuTemplateUrl){d&&d.$destroy();var F=angular.element('');m.dropdownMenu.replaceWith(F),m.dropdownMenu=F}m.selectedOption=null}angular.isFunction(b)&&b(e,i)})}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(e,t,n,o){o.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"A",require:"?^uibDropdown",link:function(e,t,n,o){if(o&&!angular.isDefined(n.dropdownNested)){t.addClass("dropdown-menu");var i=n.templateUrl;i&&(o.dropdownMenuTemplateUrl=i),o.dropdownMenu||(o.dropdownMenu=t)}}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(e,t,n,o){if(o){t.addClass("dropdown-toggle"),o.toggleElement=t;var i=function(i){i.preventDefault(),t.hasClass("disabled")||n.disabled||e.$apply(function(){o.toggle()})};t.on("click",i),t.attr({"aria-haspopup":!0,"aria-expanded":!1}),e.$watch(o.isOpen,function(e){t.attr("aria-expanded",!!e)}),e.$on("$destroy",function(){t.off("click",i)})}}}}),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var e=[];return{add:function(t,n){e.push({key:t,value:n})},get:function(t){for(var n=0;n-1&&x>e&&(e=x),e}function d(e,t){var n=k.get(e).value,o=n.appendTo;k.remove(e),M=k.top(),M&&(x=parseInt(M.value.modalDomEl.attr("index"),10)),h(n.modalDomEl,n.modalScope,function(){var t=n.openedClass||D;C.remove(t,e);var i=C.hasKey(t);o.toggleClass(t,i),!i&&y&&y.heightOverflow&&y.scrollbarWidth&&(y.originalRight?o.css({paddingRight:y.originalRight+"px"}):o.css({paddingRight:""}),y=null),f(!0)},n.closedDeferred),m(),t&&t.focus?t.focus():o.focus&&o.focus()}function f(e){var t;k.length()>0&&(t=k.top().value,t.modalDomEl.toggleClass(t.windowTopClass||"",e))}function m(){if($&&-1===c()){var e=w;h($,w,function(){e=null}),$=void 0,w=void 0}}function h(t,n,o,i){function r(){r.done||(r.done=!0,e.leave(t).then(function(){o&&o(),t.remove(),i&&i.resolve()}),n.$destroy())}var l,s=null,u=function(){return l||(l=a.defer(),s=l.promise),function(){l.resolve()}};return n.$broadcast(T.NOW_CLOSING_EVENT,u),a.when(s).then(r)}function g(e){if(e.isDefaultPrevented())return e;var t=k.top();if(t)switch(e.which){case 27:t.value.keyboard&&(e.preventDefault(),i.$apply(function(){T.dismiss(t.key,"escape key press")}));break;case 9:var n=T.loadFocusElementList(t),o=!1;e.shiftKey?(T.isFocusInFirstItem(e,n)||T.isModalFocused(e,t))&&(o=T.focusLastFocusableElement(n)):T.isFocusInLastItem(e,n)&&(o=T.focusFirstFocusableElement(n)),o&&(e.preventDefault(),e.stopPropagation())}}function v(e,t,n){return!e.value.modalScope.$broadcast("modal.closing",t,n).defaultPrevented}function b(){Array.prototype.forEach.call(document.querySelectorAll("["+O+"]"),function(e){var t=parseInt(e.getAttribute(O),10),n=t-1;e.setAttribute(O,n),n||(e.removeAttribute(O),e.removeAttribute("aria-hidden"))})}var $,w,y,D="modal-open",k=l.createNew(),C=r.createNew(),T={NOW_CLOSING_EVENT:"modal.stack.now-closing"},x=0,M=null,O="data-bootstrap-modal-aria-hidden-count",E="a[href], area[href], input:not([disabled]):not([tabindex='-1']), button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']), textarea:not([disabled]):not([tabindex='-1']), iframe, object, embed, *[tabindex]:not([tabindex='-1']), *[contenteditable=true]",P=/[A-Z]/g;return i.$watch(c,function(e){w&&(w.index=e)}),n.on("keydown",g),i.$on("$destroy",function(){n.off("keydown",g)}),T.open=function(t,a){function r(e){function t(e){var t=e.parent()?e.parent().children():[];return Array.prototype.filter.call(t,function(t){return t!==e[0]})}if(e&&"BODY"!==e[0].tagName)return t(e).forEach(function(e){var t="true"===e.getAttribute("aria-hidden"),n=parseInt(e.getAttribute(O),10);n||(n=t?1:0),e.setAttribute(O,n+1),e.setAttribute("aria-hidden","true")}),r(e.parent())}var l=n[0].activeElement,p=a.openedClass||D;f(!1),M=k.top(),k.add(t,{deferred:a.deferred,renderDeferred:a.renderDeferred,closedDeferred:a.closedDeferred,modalScope:a.scope,backdrop:a.backdrop,keyboard:a.keyboard,openedClass:a.openedClass,windowTopClass:a.windowTopClass,animation:a.animation,appendTo:a.appendTo}),C.put(p,t);var d=a.appendTo,m=c();m>=0&&!$&&(w=i.$new(!0),w.modalOptions=a,w.index=m,$=angular.element('
        '),$.attr({"class":"modal-backdrop","ng-style":"{'z-index': 1040 + (index && 1 || 0) + index*10}","uib-modal-animation-class":"fade","modal-in-class":"in"}),a.backdropClass&&$.addClass(a.backdropClass),a.animation&&$.attr("modal-animation","true"),o($)(w),e.enter($,d),s.isScrollable(d)&&(y=s.scrollbarPadding(d),y.heightOverflow&&y.scrollbarWidth&&d.css({paddingRight:y.right+"px"})));var h;a.component?(h=document.createElement(u(a.component.name)),h=angular.element(h),h.attr({resolve:"$resolve","modal-instance":"$uibModalInstance",close:"$close($value)",dismiss:"$dismiss($value)"})):h=a.content,x=M?parseInt(M.value.modalDomEl.attr("index"),10)+1:0;var g=angular.element('
        ');g.attr({"class":"modal","template-url":a.windowTemplateUrl,"window-top-class":a.windowTopClass,role:"dialog","aria-labelledby":a.ariaLabelledBy,"aria-describedby":a.ariaDescribedBy,size:a.size,index:x,animate:"animate","ng-style":"{'z-index': 1050 + $$topModalIndex*10, display: 'block'}",tabindex:-1,"uib-modal-animation-class":"fade","modal-in-class":"in"}).append(h),a.windowClass&&g.addClass(a.windowClass),a.animation&&g.attr("modal-animation","true"),d.addClass(p),a.scope&&(a.scope.$$topModalIndex=x),e.enter(o(g)(a.scope),d),k.top().value.modalDomEl=g,k.top().value.modalOpener=l,r(g)},T.close=function(e,t){var n=k.get(e);return b(),n&&v(n,t,!0)?(n.value.modalScope.$$uibDestructionScheduled=!0,n.value.deferred.resolve(t),d(e,n.value.modalOpener),!0):!n},T.dismiss=function(e,t){var n=k.get(e);return b(),n&&v(n,t,!1)?(n.value.modalScope.$$uibDestructionScheduled=!0,n.value.deferred.reject(t),d(e,n.value.modalOpener),!0):!n},T.dismissAll=function(e){for(var t=this.getTop();t&&this.dismiss(t.key,e);)t=this.getTop()},T.getTop=function(){return k.top()},T.modalRendered=function(e){var t=k.get(e);t&&t.value.renderDeferred.resolve()},T.focusFirstFocusableElement=function(e){return e.length>0?(e[0].focus(),!0):!1},T.focusLastFocusableElement=function(e){return e.length>0?(e[e.length-1].focus(),!0):!1},T.isModalFocused=function(e,t){if(e&&t){var n=t.value.modalDomEl;if(n&&n.length)return(e.target||e.srcElement)===n[0]}return!1},T.isFocusInFirstItem=function(e,t){return t.length>0?(e.target||e.srcElement)===t[0]:!1},T.isFocusInLastItem=function(e,t){return t.length>0?(e.target||e.srcElement)===t[t.length-1]:!1},T.loadFocusElementList=function(e){if(e){var t=e.value.modalDomEl;if(t&&t.length){var n=t[0].querySelectorAll(E);return n?Array.prototype.filter.call(n,function(e){return p(e)}):n}}},T}]).provider("$uibModal",function(){var e={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$rootScope","$q","$document","$templateRequest","$controller","$uibResolve","$uibModalStack",function(t,n,o,i,a,r,l){function s(e){return e.template?n.when(e.template):i(angular.isFunction(e.templateUrl)?e.templateUrl():e.templateUrl)}var u={},p=null;return u.getPromiseChain=function(){return p},u.open=function(i){function u(){return g}var c=n.defer(),d=n.defer(),f=n.defer(),m=n.defer(),h={result:c.promise,opened:d.promise,closed:f.promise,rendered:m.promise,close:function(e){return l.close(h,e)},dismiss:function(e){return l.dismiss(h,e)}};if(i=angular.extend({},e.options,i),i.resolve=i.resolve||{},i.appendTo=i.appendTo||o.find("body").eq(0),!i.appendTo.length)throw new Error("appendTo element not found. Make sure that the element passed is in DOM.");if(!i.component&&!i.template&&!i.templateUrl)throw new Error("One of component or template or templateUrl options is required.");var g;g=i.component?n.when(r.resolve(i.resolve,{},null,null)):n.all([s(i),r.resolve(i.resolve,{},null,null)]);var v;return v=p=n.all([p]).then(u,u).then(function(e){function n(t,n,o,i){t.$scope=r,t.$scope.$resolve={},o?t.$scope.$uibModalInstance=h:t.$uibModalInstance=h;var a=n?e[1]:e;angular.forEach(a,function(e,n){i&&(t[n]=e),t.$scope.$resolve[n]=e})}var o=i.scope||t,r=o.$new();r.$close=h.close,r.$dismiss=h.dismiss,r.$on("$destroy",function(){r.$$uibDestructionScheduled||r.$dismiss("$uibUnscheduledDestruction")});var s,u,p={scope:r,deferred:c,renderDeferred:m,closedDeferred:f,animation:i.animation,backdrop:i.backdrop,keyboard:i.keyboard,backdropClass:i.backdropClass,windowTopClass:i.windowTopClass,windowClass:i.windowClass,windowTemplateUrl:i.windowTemplateUrl,ariaLabelledBy:i.ariaLabelledBy,ariaDescribedBy:i.ariaDescribedBy,size:i.size,openedClass:i.openedClass,appendTo:i.appendTo},g={},v={};i.component?(n(g,!1,!0,!1),g.name=i.component,p.component=g):i.controller&&(n(v,!0,!1,!0),u=a(i.controller,v,!0,i.controllerAs),i.controllerAs&&i.bindToController&&(s=u.instance,s.$close=r.$close,s.$dismiss=r.$dismiss,angular.extend(s,{$resolve:v.$scope.$resolve},o)),s=u(),angular.isFunction(s.$onInit)&&s.$onInit()),i.component||(p.content=e[0]),l.open(h,p),d.resolve(!0)},function(e){d.reject(e),c.reject(e)})["finally"](function(){p===v&&(p=null)}),h},u}]};return e}),angular.module("ui.bootstrap.paging",[]).factory("uibPaging",["$parse",function(e){return{create:function(t,n,o){t.setNumPages=o.numPages?e(o.numPages).assign:angular.noop,t.ngModelCtrl={$setViewValue:angular.noop},t._watchers=[],t.init=function(e,i){t.ngModelCtrl=e,t.config=i,e.$render=function(){t.render()},o.itemsPerPage?t._watchers.push(n.$parent.$watch(o.itemsPerPage,function(e){t.itemsPerPage=parseInt(e,10),n.totalPages=t.calculateTotalPages(),t.updatePage()})):t.itemsPerPage=i.itemsPerPage,n.$watch("totalItems",function(e,o){(angular.isDefined(e)||e!==o)&&(n.totalPages=t.calculateTotalPages(),t.updatePage())})},t.calculateTotalPages=function(){var e=t.itemsPerPage<1?1:Math.ceil(n.totalItems/t.itemsPerPage);return Math.max(e||0,1)},t.render=function(){n.page=parseInt(t.ngModelCtrl.$viewValue,10)||1},n.selectPage=function(e,o){o&&o.preventDefault();var i=!n.ngDisabled||!o;i&&n.page!==e&&e>0&&e<=n.totalPages&&(o&&o.target&&o.target.blur(),t.ngModelCtrl.$setViewValue(e),t.ngModelCtrl.$render())},n.getText=function(e){return n[e+"Text"]||t.config[e+"Text"]},n.noPrevious=function(){return 1===n.page},n.noNext=function(){return n.page===n.totalPages},t.updatePage=function(){t.setNumPages(n.$parent,n.totalPages),n.page>n.totalPages?n.selectPage(n.totalPages):t.ngModelCtrl.$render()},n.$on("$destroy",function(){for(;t._watchers.length;)t._watchers.shift()(); + })}}}]),angular.module("ui.bootstrap.pager",["ui.bootstrap.paging","ui.bootstrap.tabindex"]).controller("UibPagerController",["$scope","$attrs","uibPaging","uibPagerConfig",function(e,t,n,o){e.align=angular.isDefined(t.align)?e.$parent.$eval(t.align):o.align,n.create(this,e,t)}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(e){return{scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],restrict:"A",controller:"UibPagerController",controllerAs:"pager",templateUrl:function(e,t){return t.templateUrl||"uib/template/pager/pager.html"},link:function(t,n,o,i){n.addClass("pager");var a=i[0],r=i[1];r&&a.init(r,e)}}}]),angular.module("ui.bootstrap.pagination",["ui.bootstrap.paging","ui.bootstrap.tabindex"]).controller("UibPaginationController",["$scope","$attrs","$parse","uibPaging","uibPaginationConfig",function(e,t,n,o,i){function a(e,t,n){return{number:e,text:t,active:n}}function r(e,t){var n=[],o=1,i=t,r=angular.isDefined(s)&&t>s;r&&(u?(o=Math.max(e-Math.floor(s/2),1),i=o+s-1,i>t&&(i=t,o=i-s+1)):(o=(Math.ceil(e/s)-1)*s+1,i=Math.min(o+s-1,t)));for(var l=o;i>=l;l++){var f=a(l,d(l),l===e);n.push(f)}if(r&&s>0&&(!u||p||c)){if(o>1){if(!c||o>3){var m=a(o-1,"...",!1);n.unshift(m)}if(c){if(3===o){var h=a(2,"2",!1);n.unshift(h)}var g=a(1,"1",!1);n.unshift(g)}}if(t>i){if(!c||t-2>i){var v=a(i+1,"...",!1);n.push(v)}if(c){if(i===t-2){var b=a(t-1,t-1,!1);n.push(b)}var $=a(t,t,!1);n.push($)}}}return n}var l=this,s=angular.isDefined(t.maxSize)?e.$parent.$eval(t.maxSize):i.maxSize,u=angular.isDefined(t.rotate)?e.$parent.$eval(t.rotate):i.rotate,p=angular.isDefined(t.forceEllipses)?e.$parent.$eval(t.forceEllipses):i.forceEllipses,c=angular.isDefined(t.boundaryLinkNumbers)?e.$parent.$eval(t.boundaryLinkNumbers):i.boundaryLinkNumbers,d=angular.isDefined(t.pageLabel)?function(n){return e.$parent.$eval(t.pageLabel,{$page:n})}:angular.identity;e.boundaryLinks=angular.isDefined(t.boundaryLinks)?e.$parent.$eval(t.boundaryLinks):i.boundaryLinks,e.directionLinks=angular.isDefined(t.directionLinks)?e.$parent.$eval(t.directionLinks):i.directionLinks,t.$set("role","menu"),o.create(this,e,t),t.maxSize&&l._watchers.push(e.$parent.$watch(n(t.maxSize),function(e){s=parseInt(e,10),l.render()}));var f=this.render;this.render=function(){f(),e.page>0&&e.page<=e.totalPages&&(e.pages=r(e.page,e.totalPages))}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,boundaryLinkNumbers:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0,forceEllipses:!1}).directive("uibPagination",["$parse","uibPaginationConfig",function(e,t){return{scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],restrict:"A",controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(e,t){return t.templateUrl||"uib/template/pagination/pagination.html"},link:function(e,n,o,i){n.addClass("pagination");var a=i[0],r=i[1];r&&a.init(r,t)}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function e(e){var t=/[A-Z]/g,n="-";return e.replace(t,function(e,t){return(t?n:"")+e.toLowerCase()})}var t={placement:"top",placementClassPrefix:"",animation:!0,popupDelay:0,popupCloseDelay:0,useContentExp:!1},n={mouseenter:"mouseleave",click:"click",outsideClick:"outsideClick",focus:"blur",none:""},o={};this.options=function(e){angular.extend(o,e)},this.setTriggers=function(e){angular.extend(n,e)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(i,a,r,l,s,u,p,c,d){function f(e){if(27===e.which){var t=m.top();t&&(t.value.close(),t=null)}}var m=d.createNew();return l.on("keyup",f),p.$on("$destroy",function(){l.off("keyup",f)}),function(i,p,d,f){function h(e){var t=(e||f.trigger||d).split(" "),o=t.map(function(e){return n[e]||e});return{show:t,hide:o}}f=angular.extend({},t,o,f);var g=e(i),v=u.startSymbol(),b=u.endSymbol(),$="
        ';return{compile:function(e,t){var n=a($);return function(e,t,o,a){function u(){z.isOpen?g():d()}function d(){(!L||e.$eval(o[p+"Enable"]))&&(w(),k(),z.popupDelay?I||(I=r(v,z.popupDelay,!1)):v())}function g(){b(),z.popupCloseDelay?U||(U=r($,z.popupCloseDelay,!1)):$()}function v(){return b(),w(),z.content?(y(),void z.$evalAsync(function(){z.isOpen=!0,C(!0),j()})):angular.noop}function b(){I&&(r.cancel(I),I=null),N&&(r.cancel(N),N=null)}function $(){z&&z.$evalAsync(function(){z&&(z.isOpen=!1,C(!1),z.animation?A||(A=r(D,150,!1)):D())})}function w(){U&&(r.cancel(U),U=null),A&&(r.cancel(A),A=null)}function y(){P||(S=z.$new(),P=n(S,function(e){R?l.find("body").append(e):t.after(e)}),m.add(z,{close:$}),T())}function D(){b(),w(),x(),P&&(P.remove(),P=null,F&&r.cancel(F)),m.remove(z),S&&(S.$destroy(),S=null)}function k(){z.title=o[p+"Title"],B?z.content=B(e):z.content=o[i],z.popupClass=o[p+"Class"],z.placement=angular.isDefined(o[p+"Placement"])?o[p+"Placement"]:f.placement;var t=s.parsePlacement(z.placement);H=t[1]?t[0]+"-"+t[1]:t[0];var n=parseInt(o[p+"PopupDelay"],10),a=parseInt(o[p+"PopupCloseDelay"],10);z.popupDelay=isNaN(n)?f.popupDelay:n,z.popupCloseDelay=isNaN(a)?f.popupCloseDelay:a}function C(t){Y&&angular.isFunction(Y.assign)&&Y.assign(e,t)}function T(){W.length=0,B?(W.push(e.$watch(B,function(e){z.content=e,!e&&z.isOpen&&$()})),W.push(S.$watch(function(){q||(q=!0,S.$$postDigest(function(){q=!1,z&&z.isOpen&&j()}))}))):W.push(o.$observe(i,function(e){z.content=e,!e&&z.isOpen?$():j()})),W.push(o.$observe(p+"Title",function(e){z.title=e,z.isOpen&&j()})),W.push(o.$observe(p+"Placement",function(e){z.placement=e?e:f.placement,z.isOpen&&j()}))}function x(){W.length&&(angular.forEach(W,function(e){e()}),W.length=0)}function M(e){z&&z.isOpen&&P&&(t[0].contains(e.target)||P[0].contains(e.target)||g())}function O(e){27===e.which&&g()}function E(){var n=[],i=[],a=e.$eval(o[p+"Trigger"]);_(),angular.isObject(a)?(Object.keys(a).forEach(function(e){n.push(e),i.push(a[e])}),V={show:n,hide:i}):V=h(a),"none"!==V.show&&V.show.forEach(function(e,n){"outsideClick"===e?(t.on("click",u),l.on("click",M)):e===V.hide[n]?t.on(e,u):e&&(t.on(e,d),t.on(V.hide[n],g)),t.on("keypress",O)})}var P,S,A,I,U,N,F,H,R=angular.isDefined(f.appendToBody)?f.appendToBody:!1,V=h(void 0),L=angular.isDefined(o[p+"Enable"]),z=e.$new(!0),q=!1,Y=angular.isDefined(o[p+"IsOpen"])?c(o[p+"IsOpen"]):!1,B=f.useContentExp?c(o[i]):!1,W=[],j=function(){P&&P.html()&&(N||(N=r(function(){var e=s.positionElements(t,P,z.placement,R),n=angular.isDefined(P.offsetHeight)?P.offsetHeight:P.prop("offsetHeight"),o=R?s.offset(t):s.position(t);P.css({top:e.top+"px",left:e.left+"px"});var i=e.placement.split("-");P.hasClass(i[0])||(P.removeClass(H.split("-")[0]),P.addClass(i[0])),P.hasClass(f.placementClassPrefix+e.placement)||(P.removeClass(f.placementClassPrefix+H),P.addClass(f.placementClassPrefix+e.placement)),F=r(function(){var e=angular.isDefined(P.offsetHeight)?P.offsetHeight:P.prop("offsetHeight"),t=s.adjustTop(i,o,n,e);t&&P.css(t),F=null},0,!1),P.hasClass("uib-position-measure")?(s.positionArrow(P,e.placement),P.removeClass("uib-position-measure")):H!==e.placement&&s.positionArrow(P,e.placement),H=e.placement,N=null},0,!1)))};z.origScope=e,z.isOpen=!1,z.contentExp=function(){return z.content},o.$observe("disabled",function(e){e&&b(),e&&z.isOpen&&$()}),Y&&e.$watch(Y,function(e){z&&!e===z.isOpen&&u()});var _=function(){V.show.forEach(function(e){"outsideClick"===e?t.off("click",u):(t.off(e,d),t.off(e,u)),t.off("keypress",O)}),V.hide.forEach(function(e){"outsideClick"===e?l.off("click",M):t.off(e,g)})};E();var G=e.$eval(o[p+"Animation"]);z.animation=angular.isDefined(G)?!!G:f.animation;var K,X=p+"AppendToBody";K=X in o&&void 0===o[X]?!0:e.$eval(o[X]),R=angular.isDefined(K)?K:R,e.$on("$destroy",function(){_(),D(),z=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(e,t,n,o){return{link:function(i,a,r){var l,s,u,p=i.$eval(r.tooltipTemplateTranscludeScope),c=0,d=function(){s&&(s.remove(),s=null),l&&(l.$destroy(),l=null),u&&(e.leave(u).then(function(){s=null}),s=u,u=null)};i.$watch(t.parseAsResourceUrl(r.uibTooltipTemplateTransclude),function(t){var r=++c;t?(o(t,!0).then(function(o){if(r===c){var i=p.$new(),s=o,f=n(s)(i,function(t){d(),e.enter(t,a)});l=i,u=f,l.$emit("$includeContentLoaded",t)}},function(){r===c&&(d(),i.$emit("$includeContentError",t))}),i.$emit("$includeContentRequested",t)):d()}),i.$on("$destroy",d)}}}]).directive("uibTooltipClasses",["$uibPosition",function(e){return{restrict:"A",link:function(t,n,o){if(t.placement){var i=e.parsePlacement(t.placement);n.addClass(i[0])}t.popupClass&&n.addClass(t.popupClass),t.animation&&n.addClass(o.tooltipAnimationClass)}}}]).directive("uibTooltipPopup",function(){return{restrict:"A",scope:{content:"@"},templateUrl:"uib/template/tooltip/tooltip-popup.html"}}).directive("uibTooltip",["$uibTooltip",function(e){return e("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{restrict:"A",scope:{contentExp:"&",originScope:"&"},templateUrl:"uib/template/tooltip/tooltip-template-popup.html"}}).directive("uibTooltipTemplate",["$uibTooltip",function(e){return e("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{restrict:"A",scope:{contentExp:"&"},templateUrl:"uib/template/tooltip/tooltip-html-popup.html"}}).directive("uibTooltipHtml",["$uibTooltip",function(e){return e("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{restrict:"A",scope:{uibTitle:"@",contentExp:"&",originScope:"&"},templateUrl:"uib/template/popover/popover-template.html"}}).directive("uibPopoverTemplate",["$uibTooltip",function(e){return e("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{restrict:"A",scope:{contentExp:"&",uibTitle:"@"},templateUrl:"uib/template/popover/popover-html.html"}}).directive("uibPopoverHtml",["$uibTooltip",function(e){return e("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{restrict:"A",scope:{uibTitle:"@",content:"@"},templateUrl:"uib/template/popover/popover.html"}}).directive("uibPopover",["$uibTooltip",function(e){return e("uibPopover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(e,t,n){function o(){return angular.isDefined(e.maxParam)?e.maxParam:n.max}var i=this,a=angular.isDefined(t.animate)?e.$parent.$eval(t.animate):n.animate;this.bars=[],e.max=o(),this.addBar=function(e,t,n){a||t.css({transition:"none"}),this.bars.push(e),e.max=o(),e.title=n&&angular.isDefined(n.title)?n.title:"progressbar",e.$watch("value",function(t){e.recalculatePercentage()}),e.recalculatePercentage=function(){var t=i.bars.reduce(function(e,t){return t.percent=+(100*t.value/t.max).toFixed(2),e+t.percent},0);t>100&&(e.percent-=t-100)},e.$on("$destroy",function(){t=null,i.removeBar(e)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1),this.bars.forEach(function(e){e.recalculatePercentage()})},e.$watch("maxParam",function(e){i.bars.forEach(function(e){e.max=o(),e.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{maxParam:"=?max"},templateUrl:"uib/template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"uib/template/progressbar/bar.html",link:function(e,t,n,o){o.addBar(e,t,n)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",maxParam:"=?max",type:"@"},templateUrl:"uib/template/progressbar/progressbar.html",link:function(e,t,n,o){o.addBar(e,angular.element(t.children()[0]),{title:n.title})}}}),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,enableReset:!0,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(e,t,n){var o={$setViewValue:angular.noop},i=this;this.init=function(i){o=i,o.$render=this.render,o.$formatters.push(function(e){return angular.isNumber(e)&&e<<0!==e&&(e=Math.round(e)),e}),this.stateOn=angular.isDefined(t.stateOn)?e.$parent.$eval(t.stateOn):n.stateOn,this.stateOff=angular.isDefined(t.stateOff)?e.$parent.$eval(t.stateOff):n.stateOff,this.enableReset=angular.isDefined(t.enableReset)?e.$parent.$eval(t.enableReset):n.enableReset;var a=angular.isDefined(t.titles)?e.$parent.$eval(t.titles):n.titles;this.titles=angular.isArray(a)&&a.length>0?a:n.titles;var r=angular.isDefined(t.ratingStates)?e.$parent.$eval(t.ratingStates):new Array(angular.isDefined(t.max)?e.$parent.$eval(t.max):n.max);e.range=this.buildTemplateObjects(r)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;n>t;t++)e[t]=angular.extend({index:t},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(t)},e[t]);return e},this.getTitle=function(e){return e>=this.titles.length?e+1:this.titles[e]},e.rate=function(t){if(!e.readonly&&t>=0&&t<=e.range.length){var n=i.enableReset&&o.$viewValue===t?0:t;o.$setViewValue(n),o.$render()}},e.enter=function(t){e.readonly||(e.value=t),e.onHover({value:t})},e.reset=function(){e.value=o.$viewValue,e.onLeave()},e.onKeydown=function(t){/(37|38|39|40)/.test(t.which)&&(t.preventDefault(),t.stopPropagation(),e.rate(e.value+(38===t.which||39===t.which?1:-1)))},this.render=function(){e.value=o.$viewValue,e.title=i.getTitle(e.value-1)}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],restrict:"A",scope:{readonly:"=?readOnly",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"uib/template/rating/rating.html",link:function(e,t,n,o){var i=o[0],a=o[1];i.init(a)}}}),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(e){function t(e){for(var t=0;tt.index?1:e.index0&&13>t:t>=0&&24>t;return n&&""!==e.hours?(e.showMeridian&&(12===t&&(t=0),e.meridian===C[1]&&(t+=12)),t):void 0}function s(){var t=+e.minutes,n=t>=0&&60>t;return n&&""!==e.minutes?t:void 0}function u(){var t=+e.seconds;return t>=0&&60>t?t:void 0}function p(e,t){return null===e?"":angular.isDefined(e)&&e.toString().length<2&&!t?"0"+e:e.toString()}function c(e){d(),k.$setViewValue(new Date(y)),f(e)}function d(){b&&b.$setValidity("hours",!0),$&&$.$setValidity("minutes",!0),w&&w.$setValidity("seconds",!0),k.$setValidity("time",!0),e.invalidHours=!1,e.invalidMinutes=!1,e.invalidSeconds=!1}function f(t){if(k.$modelValue){var n=y.getHours(),o=y.getMinutes(),i=y.getSeconds();e.showMeridian&&(n=0===n||12===n?12:n%12),e.hours="h"===t?n:p(n,!T),"m"!==t&&(e.minutes=p(o)),e.meridian=y.getHours()<12?C[0]:C[1],"s"!==t&&(e.seconds=p(i)),e.meridian=y.getHours()<12?C[0]:C[1]}else e.hours=null,e.minutes=null,e.seconds=null,e.meridian=C[0]}function m(e){y=g(y,e),c()}function h(e,t){return g(e,60*t)}function g(e,t){var n=new Date(e.getTime()+1e3*t),o=new Date(e);return o.setHours(n.getHours(),n.getMinutes(),n.getSeconds()),o}function v(){return(null===e.hours||""===e.hours)&&(null===e.minutes||""===e.minutes)&&(!e.showSeconds||e.showSeconds&&(null===e.seconds||""===e.seconds))}var b,$,w,y=new Date,D=[],k={$setViewValue:angular.noop},C=angular.isDefined(n.meridians)?e.$parent.$eval(n.meridians):r.meridians||a.DATETIME_FORMATS.AMPMS,T=angular.isDefined(n.padHours)?e.$parent.$eval(n.padHours):!0;e.tabindex=angular.isDefined(n.tabindex)?n.tabindex:0,t.removeAttr("tabindex"),this.init=function(t,o){k=t,k.$render=this.render,k.$formatters.unshift(function(e){return e?new Date(e):null});var i=o.eq(0),a=o.eq(1),l=o.eq(2);b=i.controller("ngModel"),$=a.controller("ngModel"),w=l.controller("ngModel");var s=angular.isDefined(n.mousewheel)?e.$parent.$eval(n.mousewheel):r.mousewheel;s&&this.setupMousewheelEvents(i,a,l);var u=angular.isDefined(n.arrowkeys)?e.$parent.$eval(n.arrowkeys):r.arrowkeys;u&&this.setupArrowkeyEvents(i,a,l),e.readonlyInput=angular.isDefined(n.readonlyInput)?e.$parent.$eval(n.readonlyInput):r.readonlyInput,this.setupInputEvents(i,a,l)};var x=r.hourStep;n.hourStep&&D.push(e.$parent.$watch(o(n.hourStep),function(e){x=+e}));var M=r.minuteStep;n.minuteStep&&D.push(e.$parent.$watch(o(n.minuteStep),function(e){M=+e}));var O;D.push(e.$parent.$watch(o(n.min),function(e){var t=new Date(e);O=isNaN(t)?void 0:t}));var E;D.push(e.$parent.$watch(o(n.max),function(e){var t=new Date(e);E=isNaN(t)?void 0:t}));var P=!1;n.ngDisabled&&D.push(e.$parent.$watch(o(n.ngDisabled),function(e){P=e})),e.noIncrementHours=function(){var e=h(y,60*x);return P||e>E||y>e&&O>e},e.noDecrementHours=function(){var e=h(y,60*-x);return P||O>e||e>y&&e>E},e.noIncrementMinutes=function(){var e=h(y,M);return P||e>E||y>e&&O>e},e.noDecrementMinutes=function(){var e=h(y,-M);return P||O>e||e>y&&e>E},e.noIncrementSeconds=function(){var e=g(y,S);return P||e>E||y>e&&O>e},e.noDecrementSeconds=function(){var e=g(y,-S);return P||O>e||e>y&&e>E},e.noToggleMeridian=function(){return y.getHours()<12?P||h(y,720)>E:P||h(y,-720)0};t.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementHours():e.decrementHours()),t.preventDefault()}),n.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementMinutes():e.decrementMinutes()),t.preventDefault()}),o.on("mousewheel wheel",function(t){P||e.$apply(i(t)?e.incrementSeconds():e.decrementSeconds()),t.preventDefault()})},this.setupArrowkeyEvents=function(t,n,o){t.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementHours(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementHours(),e.$apply()))}),n.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementMinutes(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementMinutes(),e.$apply()))}),o.on("keydown",function(t){P||(38===t.which?(t.preventDefault(),e.incrementSeconds(),e.$apply()):40===t.which&&(t.preventDefault(),e.decrementSeconds(),e.$apply()))})},this.setupInputEvents=function(t,n,o){if(e.readonlyInput)return e.updateHours=angular.noop,e.updateMinutes=angular.noop,void(e.updateSeconds=angular.noop);var i=function(t,n,o){k.$setViewValue(null),k.$setValidity("time",!1),angular.isDefined(t)&&(e.invalidHours=t,b&&b.$setValidity("hours",!1)),angular.isDefined(n)&&(e.invalidMinutes=n,$&&$.$setValidity("minutes",!1)),angular.isDefined(o)&&(e.invalidSeconds=o,w&&w.$setValidity("seconds",!1))};e.updateHours=function(){var e=l(),t=s();k.$setDirty(),angular.isDefined(e)&&angular.isDefined(t)?(y.setHours(e),y.setMinutes(t),O>y||y>E?i(!0):c("h")):i(!0)},t.on("blur",function(t){k.$setTouched(),v()?d():null===e.hours||""===e.hours?i(!0):!e.invalidHours&&e.hours<10&&e.$apply(function(){e.hours=p(e.hours,!T)})}),e.updateMinutes=function(){var e=s(),t=l();k.$setDirty(),angular.isDefined(e)&&angular.isDefined(t)?(y.setHours(t),y.setMinutes(e),O>y||y>E?i(void 0,!0):c("m")):i(void 0,!0)},n.on("blur",function(t){k.$setTouched(),v()?d():null===e.minutes?i(void 0,!0):!e.invalidMinutes&&e.minutes<10&&e.$apply(function(){e.minutes=p(e.minutes)})}),e.updateSeconds=function(){var e=u();k.$setDirty(),angular.isDefined(e)?(y.setSeconds(e),c("s")):i(void 0,void 0,!0)},o.on("blur",function(t){v()?d():!e.invalidSeconds&&e.seconds<10&&e.$apply(function(){e.seconds=p(e.seconds)})})},this.render=function(){var t=k.$viewValue;isNaN(t)?(k.$setValidity("time",!1),i.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(t&&(y=t),O>y||y>E?(k.$setValidity("time",!1),e.invalidHours=!0,e.invalidMinutes=!0):d(),f())},e.showSpinners=angular.isDefined(n.showSpinners)?e.$parent.$eval(n.showSpinners):r.showSpinners,e.incrementHours=function(){e.noIncrementHours()||m(60*x*60)},e.decrementHours=function(){e.noDecrementHours()||m(60*-x*60)},e.incrementMinutes=function(){e.noIncrementMinutes()||m(60*M)},e.decrementMinutes=function(){e.noDecrementMinutes()||m(60*-M)},e.incrementSeconds=function(){e.noIncrementSeconds()||m(S)},e.decrementSeconds=function(){e.noDecrementSeconds()||m(-S)},e.toggleMeridian=function(){var t=s(),n=l();e.noToggleMeridian()||(angular.isDefined(t)&&angular.isDefined(n)?m(720*(y.getHours()<12?60:-60)):e.meridian=e.meridian===C[0]?C[1]:C[0])},e.blur=function(){k.$setTouched()},e.$on("$destroy",function(){for(;D.length;)D.shift()()})}]).directive("uibTimepicker",["uibTimepickerConfig",function(e){return{require:["uibTimepicker","?^ngModel"],restrict:"A",controller:"UibTimepickerController",controllerAs:"timepicker",scope:{},templateUrl:function(t,n){return n.templateUrl||e.templateUrl},link:function(e,t,n,o){var i=o[0],a=o[1];a&&i.init(a,t.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.debounce","ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(e){var t=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(n){var o=n.match(t);if(!o)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+n+'".');return{itemName:o[3],source:e(o[4]),viewMapper:e(o[2]||o[1]),modelMapper:e(o[1])}}}}]).controller("UibTypeaheadController",["$scope","$element","$attrs","$compile","$parse","$q","$timeout","$document","$window","$rootScope","$$debounce","$uibPosition","uibTypeaheadParser",function(e,t,n,o,i,a,r,l,s,u,p,c,d){function f(){z.moveInProgress||(z.moveInProgress=!0,z.$digest()),J()}function m(){z.position=S?c.offset(t):c.position(t),z.position.top+=t.prop("offsetHeight")}function h(e){var t;return angular.version.minor<6?(t=e.$options||{},t.getOption=function(e){return t[e]}):t=e.$options,t}var g,v,b=[9,13,27,38,40],$=200,w=e.$eval(n.typeaheadMinLength);w||0===w||(w=1),e.$watch(n.typeaheadMinLength,function(e){w=e||0===e?e:1});var y=e.$eval(n.typeaheadWaitMs)||0,D=e.$eval(n.typeaheadEditable)!==!1;e.$watch(n.typeaheadEditable,function(e){D=e!==!1});var k,C,T=i(n.typeaheadLoading).assign||angular.noop,x=n.typeaheadShouldSelect?i(n.typeaheadShouldSelect):function(e,t){var n=t.$event;return 13===n.which||9===n.which},M=i(n.typeaheadOnSelect),O=angular.isDefined(n.typeaheadSelectOnBlur)?e.$eval(n.typeaheadSelectOnBlur):!1,E=i(n.typeaheadNoResults).assign||angular.noop,P=n.typeaheadInputFormatter?i(n.typeaheadInputFormatter):void 0,S=n.typeaheadAppendToBody?e.$eval(n.typeaheadAppendToBody):!1,A=n.typeaheadAppendTo?e.$eval(n.typeaheadAppendTo):null,I=e.$eval(n.typeaheadFocusFirst)!==!1,U=n.typeaheadSelectOnExact?e.$eval(n.typeaheadSelectOnExact):!1,N=i(n.typeaheadIsOpen).assign||angular.noop,F=e.$eval(n.typeaheadShowHint)||!1,H=i(n.ngModel),R=i(n.ngModel+"($$$p)"),V=function(t,n){return angular.isFunction(H(e))&&v.getOption("getterSetter")?R(t,{$$$p:n}):H.assign(t,n)},L=d.parse(n.uibTypeahead),z=e.$new(),q=e.$on("$destroy",function(){z.$destroy()});z.$on("$destroy",q);var Y="typeahead-"+z.$id+"-"+Math.floor(1e4*Math.random());t.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":Y});var B,W;F&&(B=angular.element("
        "),B.css("position","relative"),t.after(B),W=t.clone(),W.attr("placeholder",""),W.attr("tabindex","-1"),W.val(""),W.css({position:"absolute",top:"0px",left:"0px","border-color":"transparent","box-shadow":"none",opacity:1,background:"none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)",color:"#999"}),t.css({position:"relative","vertical-align":"top","background-color":"transparent"}),W.attr("id")&&W.removeAttr("id"),B.append(W),W.after(t));var j=angular.element("
        ");j.attr({id:Y,matches:"matches",active:"activeIdx",select:"select(activeIdx, evt)","move-in-progress":"moveInProgress",query:"query",position:"position","assign-is-open":"assignIsOpen(isOpen)",debounce:"debounceUpdate"}),angular.isDefined(n.typeaheadTemplateUrl)&&j.attr("template-url",n.typeaheadTemplateUrl),angular.isDefined(n.typeaheadPopupTemplateUrl)&&j.attr("popup-template-url",n.typeaheadPopupTemplateUrl);var _=function(){F&&W.val("")},G=function(){z.matches=[],z.activeIdx=-1,t.attr("aria-expanded",!1),_()},K=function(e){return Y+"-option-"+e};z.$watch("activeIdx",function(e){0>e?t.removeAttr("aria-activedescendant"):t.attr("aria-activedescendant",K(e))});var X=function(e,t){return z.matches.length>t&&e?e.toUpperCase()===z.matches[t].label.toUpperCase():!1},Z=function(n,o){var i={$viewValue:n};T(e,!0),E(e,!1),a.when(L.source(e,i)).then(function(a){var r=n===g.$viewValue;if(r&&k)if(a&&a.length>0){z.activeIdx=I?0:-1,E(e,!1),z.matches.length=0;for(var l=0;l0&&s.slice(0,n.length).toUpperCase()===n.toUpperCase()?W.val(n+s.slice(n.length)):W.val("")}}else G(),E(e,!0);r&&T(e,!1)},function(){G(),T(e,!1),E(e,!0)})};S&&(angular.element(s).on("resize",f),l.find("body").on("scroll",f));var J=p(function(){z.matches.length&&m(),z.moveInProgress=!1},$);z.moveInProgress=!1,z.query=void 0;var Q,ee=function(e){Q=r(function(){Z(e)},y)},te=function(){Q&&r.cancel(Q)};G(),z.assignIsOpen=function(t){N(e,t)},z.select=function(o,i){var a,l,s={};C=!0,s[L.itemName]=l=z.matches[o].model,a=L.modelMapper(e,s),V(e,a),g.$setValidity("editable",!0),g.$setValidity("parse",!0),M(e,{$item:l,$model:a,$label:L.viewMapper(e,s),$event:i}),G(),z.$eval(n.typeaheadFocusOnSelect)!==!1&&r(function(){t[0].focus()},0,!1)},t.on("keydown",function(t){if(0!==z.matches.length&&-1!==b.indexOf(t.which)){var n=x(e,{$event:t});if(-1===z.activeIdx&&n||9===t.which&&t.shiftKey)return G(),void z.$digest();t.preventDefault();var o;switch(t.which){case 27:t.stopPropagation(),G(),e.$digest();break;case 38:z.activeIdx=(z.activeIdx>0?z.activeIdx:z.matches.length)-1,z.$digest(),o=j[0].querySelectorAll(".uib-typeahead-match")[z.activeIdx],o.parentNode.scrollTop=o.offsetTop;break;case 40:z.activeIdx=(z.activeIdx+1)%z.matches.length,z.$digest(),o=j[0].querySelectorAll(".uib-typeahead-match")[z.activeIdx],o.parentNode.scrollTop=o.offsetTop;break;default:n&&z.$apply(function(){angular.isNumber(z.debounceUpdate)||angular.isObject(z.debounceUpdate)?p(function(){z.select(z.activeIdx,t)},angular.isNumber(z.debounceUpdate)?z.debounceUpdate:z.debounceUpdate["default"]):z.select(z.activeIdx,t)})}}}),t.on("focus",function(e){k=!0,0!==w||g.$viewValue||r(function(){Z(g.$viewValue,e)},0)}),t.on("blur",function(e){O&&z.matches.length&&-1!==z.activeIdx&&!C&&(C=!0,z.$apply(function(){angular.isObject(z.debounceUpdate)&&angular.isNumber(z.debounceUpdate.blur)?p(function(){z.select(z.activeIdx,e)},z.debounceUpdate.blur):z.select(z.activeIdx,e)})),!D&&g.$error.editable&&(g.$setViewValue(),z.$apply(function(){g.$setValidity("editable",!0),g.$setValidity("parse",!0)}),t.val("")),k=!1,C=!1});var ne=function(n){t[0]!==n.target&&3!==n.which&&0!==z.matches.length&&(G(),u.$$phase||e.$digest())};l.on("click",ne),e.$on("$destroy",function(){l.off("click",ne),(S||A)&&oe.remove(), + S&&(angular.element(s).off("resize",f),l.find("body").off("scroll",f)),j.remove(),F&&B.remove()});var oe=o(j)(z);S?l.find("body").append(oe):A?angular.element(A).eq(0).append(oe):t.after(oe),this.init=function(t){g=t,v=h(g),z.debounceUpdate=i(v.getOption("debounce"))(e),g.$parsers.unshift(function(t){return k=!0,0===w||t&&t.length>=w?y>0?(te(),ee(t)):Z(t):(T(e,!1),te(),G()),D?t:t?void g.$setValidity("editable",!1):(g.$setValidity("editable",!0),null)}),g.$formatters.push(function(t){var n,o,i={};return D||g.$setValidity("editable",!0),P?(i.$model=t,P(e,i)):(i[L.itemName]=t,n=L.viewMapper(e,i),i[L.itemName]=void 0,o=L.viewMapper(e,i),n!==o?n:t)})}}]).directive("uibTypeahead",function(){return{controller:"UibTypeaheadController",require:["ngModel","uibTypeahead"],link:function(e,t,n,o){o[1].init(o[0])}}}).directive("uibTypeaheadPopup",["$$debounce",function(e){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&",assignIsOpen:"&",debounce:"&"},replace:!0,templateUrl:function(e,t){return t.popupTemplateUrl||"uib/template/typeahead/typeahead-popup.html"},link:function(t,n,o){t.templateUrl=o.templateUrl,t.isOpen=function(){var e=t.matches.length>0;return t.assignIsOpen({isOpen:e}),e},t.isActive=function(e){return t.active===e},t.selectActive=function(e){t.active=e},t.selectMatch=function(n,o){var i=t.debounce();angular.isNumber(i)||angular.isObject(i)?e(function(){t.select({activeIdx:n,evt:o})},angular.isNumber(i)?i:i["default"]):t.select({activeIdx:n,evt:o})}}}}]).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(e,t,n){return{scope:{index:"=",match:"=",query:"="},link:function(o,i,a){var r=n(a.templateUrl)(o.$parent)||"uib/template/typeahead/typeahead-match.html";e(r).then(function(e){var n=angular.element(e.trim());i.replaceWith(n),t(n)(o)})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(e,t,n){function o(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function i(e){return/<.*>/g.test(e)}var a;return a=t.has("$sanitize"),function(t,r){return!a&&i(t)&&n.warn("Unsafe use of typeahead please use ngSanitize"),t=r?(""+t).replace(new RegExp(o(r),"gi"),"$&"):t,a||(t=e.trustAsHtml(t)),t}}]),angular.module("ui.bootstrap.carousel").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibCarouselCss&&angular.element(document).find("head").prepend(''),angular.$$uibCarouselCss=!0}),angular.module("ui.bootstrap.datepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerCss=!0}),angular.module("ui.bootstrap.position").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibPositionCss&&angular.element(document).find("head").prepend(''),angular.$$uibPositionCss=!0}),angular.module("ui.bootstrap.datepickerPopup").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerpopupCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerpopupCss=!0}),angular.module("ui.bootstrap.tooltip").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTooltipCss&&angular.element(document).find("head").prepend(''),angular.$$uibTooltipCss=!0}),angular.module("ui.bootstrap.timepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTimepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibTimepickerCss=!0}),angular.module("ui.bootstrap.typeahead").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTypeaheadCss&&angular.element(document).find("head").prepend(''),angular.$$uibTypeaheadCss=!0}); \ No newline at end of file