summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-07-31 14:51:31 +0000
committerGerrit Code Review <review@openstack.org>2015-07-31 14:51:31 +0000
commit5ee471c93059b84a57019f613993c1effcf38d07 (patch)
tree1afd2aeb5fc59b5f85d8ac970d5460f0a09bfbc8
parent97c5fd246bb7aba67510147be487afea42f1d49b (diff)
parent56b360126af452690f9d604dbc9b7c6a3cb11ae1 (diff)
Merge "Add patched version of angular-ui bootstrap lib"
-rw-r--r--extensions/enabled/_50_add_mistral_panel.py2
-rw-r--r--karma-unit.conf.js2
-rw-r--r--merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.0.js4212
-rw-r--r--merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.13.0.patched.js (renamed from merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.1.js)1383
-rw-r--r--merlin/test/js/merlin.directives.spec.js5
5 files changed, 1020 insertions, 4584 deletions
diff --git a/extensions/enabled/_50_add_mistral_panel.py b/extensions/enabled/_50_add_mistral_panel.py
index 3fd1d46..0c4b466 100644
--- a/extensions/enabled/_50_add_mistral_panel.py
+++ b/extensions/enabled/_50_add_mistral_panel.py
@@ -11,7 +11,7 @@ ADD_INSTALLED_APPS = ['merlin', 'mistral']
11ADD_PANEL = 'mistral.panel.MistralPanel' 11ADD_PANEL = 'mistral.panel.MistralPanel'
12 12
13ADD_ANGULAR_MODULES = ['merlin', 'mistral'] 13ADD_ANGULAR_MODULES = ['merlin', 'mistral']
14ADD_JS_FILES = ['merlin/js/custom-libs/ui-bootstrap-tpls-0.12.1.js', 14ADD_JS_FILES = ['merlin/js/custom-libs/ui-bootstrap-tpls-0.13.0.patched.js',
15 'merlin/js/merlin.init.js', 15 'merlin/js/merlin.init.js',
16 'merlin/js/merlin.templates.js', 16 'merlin/js/merlin.templates.js',
17 'mistral/js/mistral.init.js'] 17 'mistral/js/mistral.init.js']
diff --git a/karma-unit.conf.js b/karma-unit.conf.js
index 7afe2f0..92dc340 100644
--- a/karma-unit.conf.js
+++ b/karma-unit.conf.js
@@ -43,7 +43,7 @@ module.exports = function (config) {
43 'merlin/static/merlin/libs/underscore/underscore-min.js', 43 'merlin/static/merlin/libs/underscore/underscore-min.js',
44 'merlin/static/merlin/libs/js-yaml/dist/js-yaml.min.js', 44 'merlin/static/merlin/libs/js-yaml/dist/js-yaml.min.js',
45 'merlin/static/merlin/js/custom-libs/barricade.js', 45 'merlin/static/merlin/js/custom-libs/barricade.js',
46 'merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.1.js', 46 'merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.13.0.patched.js',
47 // explicitly require first module definition file to avoid errors 47 // explicitly require first module definition file to avoid errors
48 'merlin/static/merlin/js/merlin.init.js', 48 'merlin/static/merlin/js/merlin.init.js',
49 'merlin/static/merlin/js/merlin.*.js', 49 'merlin/static/merlin/js/merlin.*.js',
diff --git a/merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.0.js b/merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.0.js
deleted file mode 100644
index f0cdbb0..0000000
--- a/merlin/static/merlin/js/custom-libs/ui-bootstrap-tpls-0.12.0.js
+++ /dev/null
@@ -1,4212 +0,0 @@
1/*
2 * angular-ui-bootstrap
3 * http://angular-ui.github.io/bootstrap/
4
5 * Version: 0.12.0 - 2014-11-16
6 * License: MIT
7 */
8angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","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.typeahead"]);
9angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]);
10angular.module('ui.bootstrap.transition', [])
11
12/**
13 * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
14 * @param {DOMElement} element The DOMElement that will be animated.
15 * @param {string|object|function} trigger The thing that will cause the transition to start:
16 * - As a string, it represents the css class to be added to the element.
17 * - As an object, it represents a hash of style attributes to be applied to the element.
18 * - As a function, it represents a function to be called that will cause the transition to occur.
19 * @return {Promise} A promise that is resolved when the transition finishes.
20 */
21.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
22
23 var $transition = function(element, trigger, options) {
24 options = options || {};
25 var deferred = $q.defer();
26 var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
27
28 var transitionEndHandler = function(event) {
29 $rootScope.$apply(function() {
30 element.unbind(endEventName, transitionEndHandler);
31 deferred.resolve(element);
32 });
33 };
34
35 if (endEventName) {
36 element.bind(endEventName, transitionEndHandler);
37 }
38
39 // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
40 $timeout(function() {
41 if ( angular.isString(trigger) ) {
42 element.addClass(trigger);
43 } else if ( angular.isFunction(trigger) ) {
44 trigger(element);
45 } else if ( angular.isObject(trigger) ) {
46 element.css(trigger);
47 }
48 //If browser does not support transitions, instantly resolve
49 if ( !endEventName ) {
50 deferred.resolve(element);
51 }
52 });
53
54 // Add our custom cancel function to the promise that is returned
55 // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
56 // i.e. it will therefore never raise a transitionEnd event for that transition
57 deferred.promise.cancel = function() {
58 if ( endEventName ) {
59 element.unbind(endEventName, transitionEndHandler);
60 }
61 deferred.reject('Transition cancelled');
62 };
63
64 return deferred.promise;
65 };
66
67 // Work out the name of the transitionEnd event
68 var transElement = document.createElement('trans');
69 var transitionEndEventNames = {
70 'WebkitTransition': 'webkitTransitionEnd',
71 'MozTransition': 'transitionend',
72 'OTransition': 'oTransitionEnd',
73 'transition': 'transitionend'
74 };
75 var animationEndEventNames = {
76 'WebkitTransition': 'webkitAnimationEnd',
77 'MozTransition': 'animationend',
78 'OTransition': 'oAnimationEnd',
79 'transition': 'animationend'
80 };
81 function findEndEventName(endEventNames) {
82 for (var name in endEventNames){
83 if (transElement.style[name] !== undefined) {
84 return endEventNames[name];
85 }
86 }
87 }
88 $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
89 $transition.animationEndEventName = findEndEventName(animationEndEventNames);
90 return $transition;
91}]);
92
93angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition'])
94
95 .directive('collapse', ['$transition', function ($transition) {
96
97 return {
98 link: function (scope, element, attrs) {
99
100 var initialAnimSkip = true;
101 var currentTransition;
102
103 function doTransition(change) {
104 var newTransition = $transition(element, change);
105 if (currentTransition) {
106 currentTransition.cancel();
107 }
108 currentTransition = newTransition;
109 newTransition.then(newTransitionDone, newTransitionDone);
110 return newTransition;
111
112 function newTransitionDone() {
113 // Make sure it's this transition, otherwise, leave it alone.
114 if (currentTransition === newTransition) {
115 currentTransition = undefined;
116 }
117 }
118 }
119
120 function expand() {
121 if (initialAnimSkip) {
122 initialAnimSkip = false;
123 expandDone();
124 } else {
125 element.removeClass('collapse').addClass('collapsing');
126 doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone);
127 }
128 }
129
130 function expandDone() {
131 element.removeClass('collapsing');
132 element.addClass('collapse in');
133 element.css({height: 'auto'});
134 }
135
136 function collapse() {
137 if (initialAnimSkip) {
138 initialAnimSkip = false;
139 collapseDone();
140 element.css({height: 0});
141 } else {
142 // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value
143 element.css({ height: element[0].scrollHeight + 'px' });
144 //trigger reflow so a browser realizes that height was updated from auto to a specific value
145 var x = element[0].offsetWidth;
146
147 element.removeClass('collapse in').addClass('collapsing');
148
149 doTransition({ height: 0 }).then(collapseDone);
150 }
151 }
152
153 function collapseDone() {
154 element.removeClass('collapsing');
155 element.addClass('collapse');
156 }
157
158 scope.$watch(attrs.collapse, function (shouldCollapse) {
159 if (shouldCollapse) {
160 collapse();
161 } else {
162 expand();
163 }
164 });
165 }
166 };
167 }]);
168
169angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
170
171.constant('accordionConfig', {
172 closeOthers: true
173})
174
175.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
176
177 // This array keeps track of the accordion groups
178 this.groups = [];
179
180 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
181 this.closeOthers = function(openGroup) {
182 var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
183 if ( closeOthers ) {
184 angular.forEach(this.groups, function (group) {
185 if ( group !== openGroup ) {
186 group.isOpen = false;
187 }
188 });
189 }
190 };
191
192 // This is called from the accordion-group directive to add itself to the accordion
193 this.addGroup = function(groupScope) {
194 var that = this;
195 this.groups.push(groupScope);
196
197 groupScope.$on('$destroy', function (event) {
198 that.removeGroup(groupScope);
199 });
200 };
201
202 // This is called from the accordion-group directive when to remove itself
203 this.removeGroup = function(group) {
204 var index = this.groups.indexOf(group);
205 if ( index !== -1 ) {
206 this.groups.splice(index, 1);
207 }
208 };
209
210}])
211
212// The accordion directive simply sets up the directive controller
213// and adds an accordion CSS class to itself element.
214.directive('accordion', function () {
215 return {
216 restrict:'EA',
217 controller:'AccordionController',
218 transclude: true,
219 replace: false,
220 templateUrl: 'template/accordion/accordion.html'
221 };
222})
223
224// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
225.directive('accordionGroup', function() {
226 return {
227 require:'^accordion', // We need this directive to be inside an accordion
228 restrict:'EA',
229 transclude:true, // It transcludes the contents of the directive into the template
230 replace: true, // The element containing the directive will be replaced with the template
231 templateUrl:'template/accordion/accordion-group.html',
232 scope: {
233 heading: '@', // Interpolate the heading attribute onto this scope
234 isOpen: '=?',
235 isDisabled: '=?'
236 },
237 controller: function() {
238 this.setHeading = function(element) {
239 this.heading = element;
240 };
241 },
242 link: function(scope, element, attrs, accordionCtrl) {
243 accordionCtrl.addGroup(scope);
244
245 scope.$watch('isOpen', function(value) {
246 if ( value ) {
247 accordionCtrl.closeOthers(scope);
248 }
249 });
250
251 scope.toggleOpen = function() {
252 if ( !scope.isDisabled ) {
253 scope.isOpen = !scope.isOpen;
254 }
255 };
256 }
257 };
258})
259
260// Use accordion-heading below an accordion-group to provide a heading containing HTML
261// <accordion-group>
262// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
263// </accordion-group>
264.directive('accordionHeading', function() {
265 return {
266 restrict: 'EA',
267 transclude: true, // Grab the contents to be used as the heading
268 template: '', // In effect remove this element!
269 replace: true,
270 require: '^accordionGroup',
271 link: function(scope, element, attr, accordionGroupCtrl, transclude) {
272 // Pass the heading to the accordion-group controller
273 // so that it can be transcluded into the right place in the template
274 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
275 accordionGroupCtrl.setHeading(transclude(scope, function() {}));
276 }
277 };
278})
279
280// Use in the accordion-group template to indicate where you want the heading to be transcluded
281// You must provide the property on the accordion-group controller that will hold the transcluded element
282// <div class="accordion-group">
283// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
284// ...
285// </div>
286.directive('accordionTransclude', function() {
287 return {
288 require: '^accordionGroup',
289 link: function(scope, element, attr, controller) {
290 scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
291 if ( heading ) {
292 element.html('');
293 element.append(heading);
294 }
295 });
296 }
297 };
298});
299
300angular.module('ui.bootstrap.alert', [])
301
302.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
303 $scope.closeable = 'close' in $attrs;
304 this.close = $scope.close;
305}])
306
307.directive('alert', function () {
308 return {
309 restrict:'EA',
310 controller:'AlertController',
311 templateUrl:'template/alert/alert.html',
312 transclude:true,
313 replace:true,
314 scope: {
315 type: '@',
316 close: '&'
317 }
318 };
319})
320
321.directive('dismissOnTimeout', ['$timeout', function($timeout) {
322 return {
323 require: 'alert',
324 link: function(scope, element, attrs, alertCtrl) {
325 $timeout(function(){
326 alertCtrl.close();
327 }, parseInt(attrs.dismissOnTimeout, 10));
328 }
329 };
330}]);
331
332angular.module('ui.bootstrap.bindHtml', [])
333
334 .directive('bindHtmlUnsafe', function () {
335 return function (scope, element, attr) {
336 element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
337 scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
338 element.html(value || '');
339 });
340 };
341 });
342angular.module('ui.bootstrap.buttons', [])
343
344.constant('buttonConfig', {
345 activeClass: 'active',
346 toggleEvent: 'click'
347})
348
349.controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
350 this.activeClass = buttonConfig.activeClass || 'active';
351 this.toggleEvent = buttonConfig.toggleEvent || 'click';
352}])
353
354.directive('btnRadio', function () {
355 return {
356 require: ['btnRadio', 'ngModel'],
357 controller: 'ButtonsController',
358 link: function (scope, element, attrs, ctrls) {
359 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
360
361 //model -> UI
362 ngModelCtrl.$render = function () {
363 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
364 };
365
366 //ui->model
367 element.bind(buttonsCtrl.toggleEvent, function () {
368 var isActive = element.hasClass(buttonsCtrl.activeClass);
369
370 if (!isActive || angular.isDefined(attrs.uncheckable)) {
371 scope.$apply(function () {
372 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
373 ngModelCtrl.$render();
374 });
375 }
376 });
377 }
378 };
379})
380
381.directive('btnCheckbox', function () {
382 return {
383 require: ['btnCheckbox', 'ngModel'],
384 controller: 'ButtonsController',
385 link: function (scope, element, attrs, ctrls) {
386 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
387
388 function getTrueValue() {
389 return getCheckboxValue(attrs.btnCheckboxTrue, true);
390 }
391
392 function getFalseValue() {
393 return getCheckboxValue(attrs.btnCheckboxFalse, false);
394 }
395
396 function getCheckboxValue(attributeValue, defaultValue) {
397 var val = scope.$eval(attributeValue);
398 return angular.isDefined(val) ? val : defaultValue;
399 }
400
401 //model -> UI
402 ngModelCtrl.$render = function () {
403 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
404 };
405
406 //ui->model
407 element.bind(buttonsCtrl.toggleEvent, function () {
408 scope.$apply(function () {
409 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
410 ngModelCtrl.$render();
411 });
412 });
413 }
414 };
415});
416
417/**
418* @ngdoc overview
419* @name ui.bootstrap.carousel
420*
421* @description
422* AngularJS version of an image carousel.
423*
424*/
425angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
426.controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) {
427 var self = this,
428 slides = self.slides = $scope.slides = [],
429 currentIndex = -1,
430 currentInterval, isPlaying;
431 self.currentSlide = null;
432
433 var destroyed = false;
434 /* direction: "prev" or "next" */
435 self.select = $scope.select = function(nextSlide, direction) {
436 var nextIndex = slides.indexOf(nextSlide);
437 //Decide direction if it's not given
438 if (direction === undefined) {
439 direction = nextIndex > currentIndex ? 'next' : 'prev';
440 }
441 if (nextSlide && nextSlide !== self.currentSlide) {
442 if ($scope.$currentTransition) {
443 $scope.$currentTransition.cancel();
444 //Timeout so ng-class in template has time to fix classes for finished slide
445 $timeout(goNext);
446 } else {
447 goNext();
448 }
449 }
450 function goNext() {
451 // Scope has been destroyed, stop here.
452 if (destroyed) { return; }
453 //If we have a slide to transition from and we have a transition type and we're allowed, go
454 if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) {
455 //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime
456 nextSlide.$element.addClass(direction);
457 var reflow = nextSlide.$element[0].offsetWidth; //force reflow
458
459 //Set all other slides to stop doing their stuff for the new transition
460 angular.forEach(slides, function(slide) {
461 angular.extend(slide, {direction: '', entering: false, leaving: false, active: false});
462 });
463 angular.extend(nextSlide, {direction: direction, active: true, entering: true});
464 angular.extend(self.currentSlide||{}, {direction: direction, leaving: true});
465
466 $scope.$currentTransition = $transition(nextSlide.$element, {});
467 //We have to create new pointers inside a closure since next & current will change
468 (function(next,current) {
469 $scope.$currentTransition.then(
470 function(){ transitionDone(next, current); },
471 function(){ transitionDone(next, current); }
472 );
473 }(nextSlide, self.currentSlide));
474 } else {
475 transitionDone(nextSlide, self.currentSlide);
476 }
477 self.currentSlide = nextSlide;
478 currentIndex = nextIndex;
479 //every time you change slides, reset the timer
480 restartTimer();
481 }
482 function transitionDone(next, current) {
483 angular.extend(next, {direction: '', active: true, leaving: false, entering: false});
484 angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false});
485 $scope.$currentTransition = null;
486 }
487 };
488 $scope.$on('$destroy', function () {
489 destroyed = true;
490 });
491
492 /* Allow outside people to call indexOf on slides array */
493 self.indexOfSlide = function(slide) {
494 return slides.indexOf(slide);
495 };
496
497 $scope.next = function() {
498 var newIndex = (currentIndex + 1) % slides.length;
499
500 //Prevent this user-triggered transition from occurring if there is already one in progress
501 if (!$scope.$currentTransition) {
502 return self.select(slides[newIndex], 'next');
503 }
504 };
505
506 $scope.prev = function() {
507 var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1;
508
509 //Prevent this user-triggered transition from occurring if there is already one in progress
510 if (!$scope.$currentTransition) {
511 return self.select(slides[newIndex], 'prev');
512 }
513 };
514
515 $scope.isActive = function(slide) {
516 return self.currentSlide === slide;
517 };
518
519 $scope.$watch('interval', restartTimer);
520 $scope.$on('$destroy', resetTimer);
521
522 function restartTimer() {
523 resetTimer();
524 var interval = +$scope.interval;
525 if (!isNaN(interval) && interval > 0) {
526 currentInterval = $interval(timerFn, interval);
527 }
528 }
529
530 function resetTimer() {
531 if (currentInterval) {
532 $interval.cancel(currentInterval);
533 currentInterval = null;
534 }
535 }
536
537 function timerFn() {
538 var interval = +$scope.interval;
539 if (isPlaying && !isNaN(interval) && interval > 0) {
540 $scope.next();
541 } else {
542 $scope.pause();
543 }
544 }
545
546 $scope.play = function() {
547 if (!isPlaying) {
548 isPlaying = true;
549 restartTimer();
550 }
551 };
552 $scope.pause = function() {
553 if (!$scope.noPause) {
554 isPlaying = false;
555 resetTimer();
556 }
557 };
558
559 self.addSlide = function(slide, element) {
560 slide.$element = element;
561 slides.push(slide);
562 //if this is the first slide or the slide is set to active, select it
563 if(slides.length === 1 || slide.active) {
564 self.select(slides[slides.length-1]);
565 if (slides.length == 1) {
566 $scope.play();
567 }
568 } else {
569 slide.active = false;
570 }
571 };
572
573 self.removeSlide = function(slide) {
574 //get the index of the slide inside the carousel
575 var index = slides.indexOf(slide);
576 slides.splice(index, 1);
577 if (slides.length > 0 && slide.active) {
578 if (index >= slides.length) {
579 self.select(slides[index-1]);
580 } else {
581 self.select(slides[index]);
582 }
583 } else if (currentIndex > index) {
584 currentIndex--;
585 }
586 };
587
588}])
589
590/**
591 * @ngdoc directive
592 * @name ui.bootstrap.carousel.directive:carousel
593 * @restrict EA
594 *
595 * @description
596 * Carousel is the outer container for a set of image 'slides' to showcase.
597 *
598 * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
599 * @param {boolean=} noTransition Whether to disable transitions on the carousel.
600 * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
601 *
602 * @example
603<example module="ui.bootstrap">
604 <file name="index.html">
605 <carousel>
606 <slide>
607 <img src="http://placekitten.com/150/150" style="margin:auto;">
608 <div class="carousel-caption">
609 <p>Beautiful!</p>
610 </div>
611 </slide>
612 <slide>
613 <img src="http://placekitten.com/100/150" style="margin:auto;">
614 <div class="carousel-caption">
615 <p>D'aww!</p>
616 </div>
617 </slide>
618 </carousel>
619 </file>
620 <file name="demo.css">
621 .carousel-indicators {
622 top: auto;
623 bottom: 15px;
624 }
625 </file>
626</example>
627 */
628.directive('carousel', [function() {
629 return {
630 restrict: 'EA',
631 transclude: true,
632 replace: true,
633 controller: 'CarouselController',
634 require: 'carousel',
635 templateUrl: 'template/carousel/carousel.html',
636 scope: {
637 interval: '=',
638 noTransition: '=',
639 noPause: '='
640 }
641 };
642}])
643
644/**
645 * @ngdoc directive
646 * @name ui.bootstrap.carousel.directive:slide
647 * @restrict EA
648 *
649 * @description
650 * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
651 *
652 * @param {boolean=} active Model binding, whether or not this slide is currently active.
653 *
654 * @example
655<example module="ui.bootstrap">
656 <file name="index.html">
657<div ng-controller="CarouselDemoCtrl">
658 <carousel>
659 <slide ng-repeat="slide in slides" active="slide.active">
660 <img ng-src="{{slide.image}}" style="margin:auto;">
661 <div class="carousel-caption">
662 <h4>Slide {{$index}}</h4>
663 <p>{{slide.text}}</p>
664 </div>
665 </slide>
666 </carousel>
667 Interval, in milliseconds: <input type="number" ng-model="myInterval">
668 <br />Enter a negative number to stop the interval.
669</div>
670 </file>
671 <file name="script.js">
672function CarouselDemoCtrl($scope) {
673 $scope.myInterval = 5000;
674}
675 </file>
676 <file name="demo.css">
677 .carousel-indicators {
678 top: auto;
679 bottom: 15px;
680 }
681 </file>
682</example>
683*/
684
685.directive('slide', function() {
686 return {
687 require: '^carousel',
688 restrict: 'EA',
689 transclude: true,
690 replace: true,
691 templateUrl: 'template/carousel/slide.html',
692 scope: {
693 active: '=?'
694 },
695 link: function (scope, element, attrs, carouselCtrl) {
696 carouselCtrl.addSlide(scope, element);
697 //when the scope is destroyed then remove the slide from the current slides array
698 scope.$on('$destroy', function() {
699 carouselCtrl.removeSlide(scope);
700 });
701
702 scope.$watch('active', function(active) {
703 if (active) {
704 carouselCtrl.select(scope);
705 }
706 });
707 }
708 };
709});
710
711angular.module('ui.bootstrap.dateparser', [])
712
713.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
714
715 this.parsers = {};
716
717 var formatCodeToRegex = {
718 'yyyy': {
719 regex: '\\d{4}',
720 apply: function(value) { this.year = +value; }
721 },
722 'yy': {
723 regex: '\\d{2}',
724 apply: function(value) { this.year = +value + 2000; }
725 },
726 'y': {
727 regex: '\\d{1,4}',
728 apply: function(value) { this.year = +value; }
729 },
730 'MMMM': {
731 regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
732 apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
733 },
734 'MMM': {
735 regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
736 apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
737 },
738 'MM': {
739 regex: '0[1-9]|1[0-2]',
740 apply: function(value) { this.month = value - 1; }
741 },
742 'M': {
743 regex: '[1-9]|1[0-2]',
744 apply: function(value) { this.month = value - 1; }
745 },
746 'dd': {
747 regex: '[0-2][0-9]{1}|3[0-1]{1}',
748 apply: function(value) { this.date = +value; }
749 },
750 'd': {
751 regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
752 apply: function(value) { this.date = +value; }
753 },
754 'EEEE': {
755 regex: $locale.DATETIME_FORMATS.DAY.join('|')
756 },
757 'EEE': {
758 regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
759 }
760 };
761
762 function createParser(format) {
763 var map = [], regex = format.split('');
764
765 angular.forEach(formatCodeToRegex, function(data, code) {
766 var index = format.indexOf(code);
767
768 if (index > -1) {
769 format = format.split('');
770
771 regex[index] = '(' + data.regex + ')';
772 format[index] = '$'; // Custom symbol to define consumed part of format
773 for (var i = index + 1, n = index + code.length; i < n; i++) {
774 regex[i] = '';
775 format[i] = '$';
776 }
777 format = format.join('');
778
779 map.push({ index: index, apply: data.apply });
780 }
781 });
782
783 return {
784 regex: new RegExp('^' + regex.join('') + '$'),
785 map: orderByFilter(map, 'index')
786 };
787 }
788
789 this.parse = function(input, format) {
790 if ( !angular.isString(input) || !format ) {
791 return input;
792 }
793
794 format = $locale.DATETIME_FORMATS[format] || format;
795
796 if ( !this.parsers[format] ) {
797 this.parsers[format] = createParser(format);
798 }
799
800 var parser = this.parsers[format],
801 regex = parser.regex,
802 map = parser.map,
803 results = input.match(regex);
804
805 if ( results && results.length ) {
806 var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
807
808 for( var i = 1, n = results.length; i < n; i++ ) {
809 var mapper = map[i-1];
810 if ( mapper.apply ) {
811 mapper.apply.call(fields, results[i]);
812 }
813 }
814
815 if ( isValid(fields.year, fields.month, fields.date) ) {
816 dt = new Date( fields.year, fields.month, fields.date, fields.hours);
817 }
818
819 return dt;
820 }
821 };
822
823 // Check if date is valid for specific month (and year for February).
824 // Month: 0 = Jan, 1 = Feb, etc
825 function isValid(year, month, date) {
826 if ( month === 1 && date > 28) {
827 return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
828 }
829
830 if ( month === 3 || month === 5 || month === 8 || month === 10) {
831 return date < 31;
832 }
833
834 return true;
835 }
836}]);
837
838angular.module('ui.bootstrap.position', [])
839
840/**
841 * A set of utility methods that can be use to retrieve position of DOM elements.
842 * It is meant to be used where we need to absolute-position DOM elements in
843 * relation to other, existing elements (this is the case for tooltips, popovers,
844 * typeahead suggestions etc.).
845 */
846 .factory('$position', ['$document', '$window', function ($document, $window) {
847
848 function getStyle(el, cssprop) {
849 if (el.currentStyle) { //IE
850 return el.currentStyle[cssprop];
851 } else if ($window.getComputedStyle) {
852 return $window.getComputedStyle(el)[cssprop];
853 }
854 // finally try and get inline style
855 return el.style[cssprop];
856 }
857
858 /**
859 * Checks if a given element is statically positioned
860 * @param element - raw DOM element
861 */
862 function isStaticPositioned(element) {
863 return (getStyle(element, 'position') || 'static' ) === 'static';
864 }
865
866 /**
867 * returns the closest, non-statically positioned parentOffset of a given element
868 * @param element
869 */
870 var parentOffsetEl = function (element) {
871 var docDomEl = $document[0];
872 var offsetParent = element.offsetParent || docDomEl;
873 while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
874 offsetParent = offsetParent.offsetParent;
875 }
876 return offsetParent || docDomEl;
877 };
878
879 return {
880 /**
881 * Provides read-only equivalent of jQuery's position function:
882 * http://api.jquery.com/position/
883 */
884 position: function (element) {
885 var elBCR = this.offset(element);
886 var offsetParentBCR = { top: 0, left: 0 };
887 var offsetParentEl = parentOffsetEl(element[0]);
888 if (offsetParentEl != $document[0]) {
889 offsetParentBCR = this.offset(angular.element(offsetParentEl));
890 offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
891 offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
892 }
893
894 var boundingClientRect = element[0].getBoundingClientRect();
895 return {
896 width: boundingClientRect.width || element.prop('offsetWidth'),
897 height: boundingClientRect.height || element.prop('offsetHeight'),
898 top: elBCR.top - offsetParentBCR.top,
899 left: elBCR.left - offsetParentBCR.left
900 };
901 },
902
903 /**
904 * Provides read-only equivalent of jQuery's offset function:
905 * http://api.jquery.com/offset/
906 */
907 offset: function (element) {
908 var boundingClientRect = element[0].getBoundingClientRect();
909 return {
910 width: boundingClientRect.width || element.prop('offsetWidth'),
911 height: boundingClientRect.height || element.prop('offsetHeight'),
912 top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
913 left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
914 };
915 },
916
917 /**
918 * Provides coordinates for the targetEl in relation to hostEl
919 */
920 positionElements: function (hostEl, targetEl, positionStr, appendToBody) {
921
922 var positionStrParts = positionStr.split('-');
923 var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
924
925 var hostElPos,
926 targetElWidth,
927 targetElHeight,
928 targetElPos;
929
930 hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
931
932 targetElWidth = targetEl.prop('offsetWidth');
933 targetElHeight = targetEl.prop('offsetHeight');
934
935 var shiftWidth = {
936 center: function () {
937 return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
938 },
939 left: function () {
940 return hostElPos.left;
941 },
942 right: function () {
943 return hostElPos.left + hostElPos.width;
944 }
945 };
946
947 var shiftHeight = {
948 center: function () {
949 return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
950 },
951 top: function () {
952 return hostElPos.top;
953 },
954 bottom: function () {
955 return hostElPos.top + hostElPos.height;
956 }
957 };
958
959 switch (pos0) {
960 case 'right':
961 targetElPos = {
962 top: shiftHeight[pos1](),
963 left: shiftWidth[pos0]()
964 };
965 break;
966 case 'left':
967 targetElPos = {
968 top: shiftHeight[pos1](),
969 left: hostElPos.left - targetElWidth
970 };
971 break;
972 case 'bottom':
973 targetElPos = {
974 top: shiftHeight[pos0](),
975 left: shiftWidth[pos1]()
976 };
977 break;
978 default:
979 targetElPos = {
980 top: hostElPos.top - targetElHeight,
981 left: shiftWidth[pos1]()
982 };
983 break;
984 }
985
986 return targetElPos;
987 }
988 };
989 }]);
990
991angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
992
993.constant('datepickerConfig', {
994 formatDay: 'dd',
995 formatMonth: 'MMMM',
996 formatYear: 'yyyy',
997 formatDayHeader: 'EEE',
998 formatDayTitle: 'MMMM yyyy',
999 formatMonthTitle: 'yyyy',
1000 datepickerMode: 'day',
1001 minMode: 'day',
1002 maxMode: 'year',
1003 showWeeks: true,
1004 startingDay: 0,
1005 yearRange: 20,
1006 minDate: null,
1007 maxDate: null
1008})
1009
1010.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
1011 var self = this,
1012 ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
1013
1014 // Modes chain
1015 this.modes = ['day', 'month', 'year'];
1016
1017 // Configuration attributes
1018 angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1019 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) {
1020 self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1021 });
1022
1023 // Watchable date attributes
1024 angular.forEach(['minDate', 'maxDate'], function( key ) {
1025 if ( $attrs[key] ) {
1026 $scope.$parent.$watch($parse($attrs[key]), function(value) {
1027 self[key] = value ? new Date(value) : null;
1028 self.refreshView();
1029 });
1030 } else {
1031 self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
1032 }
1033 });
1034
1035 $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1036 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1037 this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date();
1038
1039 $scope.isActive = function(dateObject) {
1040 if (self.compare(dateObject.date, self.activeDate) === 0) {
1041 $scope.activeDateId = dateObject.uid;
1042 return true;
1043 }
1044 return false;
1045 };
1046
1047 this.init = function( ngModelCtrl_ ) {
1048 ngModelCtrl = ngModelCtrl_;
1049
1050 ngModelCtrl.$render = function() {
1051 self.render();
1052 };
1053 };
1054
1055 this.render = function() {
1056 if ( ngModelCtrl.$modelValue ) {
1057 var date = new Date( ngModelCtrl.$modelValue ),
1058 isValid = !isNaN(date);
1059
1060 if ( isValid ) {
1061 this.activeDate = date;
1062 } else {
1063 $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.');
1064 }
1065 ngModelCtrl.$setValidity('date', isValid);
1066 }
1067 this.refreshView();
1068 };
1069
1070 this.refreshView = function() {
1071 if ( this.element ) {
1072 this._refreshView();
1073
1074 var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1075 ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
1076 }
1077 };
1078
1079 this.createDateObject = function(date, format) {
1080 var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1081 return {
1082 date: date,
1083 label: dateFilter(date, format),
1084 selected: model && this.compare(date, model) === 0,
1085 disabled: this.isDisabled(date),
1086 current: this.compare(date, new Date()) === 0
1087 };
1088 };
1089
1090 this.isDisabled = function( date ) {
1091 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})));
1092 };
1093
1094 // Split array into smaller arrays
1095 this.split = function(arr, size) {
1096 var arrays = [];
1097 while (arr.length > 0) {
1098 arrays.push(arr.splice(0, size));
1099 }
1100 return arrays;
1101 };
1102
1103 $scope.select = function( date ) {
1104 if ( $scope.datepickerMode === self.minMode ) {
1105 var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1106 dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1107 ngModelCtrl.$setViewValue( dt );
1108 ngModelCtrl.$render();
1109 } else {
1110 self.activeDate = date;
1111 $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
1112 }
1113 };
1114
1115 $scope.move = function( direction ) {
1116 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1117 month = self.activeDate.getMonth() + direction * (self.step.months || 0);
1118 self.activeDate.setFullYear(year, month, 1);
1119 self.refreshView();
1120 };
1121
1122 $scope.toggleMode = function( direction ) {
1123 direction = direction || 1;
1124
1125 if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
1126 return;
1127 }
1128
1129 $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ];
1130 };
1131
1132 // Key event mapper
1133 $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
1134
1135 var focusElement = function() {
1136 $timeout(function() {
1137 self.element[0].focus();
1138 }, 0 , false);
1139 };
1140
1141 // Listen for focus requests from popup directive
1142 $scope.$on('datepicker.focus', focusElement);
1143
1144 $scope.keydown = function( evt ) {
1145 var key = $scope.keys[evt.which];
1146
1147 if ( !key || evt.shiftKey || evt.altKey ) {
1148 return;
1149 }
1150
1151 evt.preventDefault();
1152 evt.stopPropagation();
1153
1154 if (key === 'enter' || key === 'space') {
1155 if ( self.isDisabled(self.activeDate)) {
1156 return; // do nothing
1157 }
1158 $scope.select(self.activeDate);
1159 focusElement();
1160 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
1161 $scope.toggleMode(key === 'up' ? 1 : -1);
1162 focusElement();
1163 } else {
1164 self.handleKeyDown(key, evt);
1165 self.refreshView();
1166 }
1167 };
1168}])
1169
1170.directive( 'datepicker', function () {
1171 return {
1172 restrict: 'EA',
1173 replace: true,
1174 templateUrl: 'template/datepicker/datepicker.html',
1175 scope: {
1176 datepickerMode: '=?',
1177 dateDisabled: '&'
1178 },
1179 require: ['datepicker', '?^ngModel'],
1180 controller: 'DatepickerController',
1181 link: function(scope, element, attrs, ctrls) {
1182 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
1183
1184 if ( ngModelCtrl ) {
1185 datepickerCtrl.init( ngModelCtrl );
1186 }
1187 }
1188 };
1189})
1190
1191.directive('daypicker', ['dateFilter', function (dateFilter) {
1192 return {
1193 restrict: 'EA',
1194 replace: true,
1195 templateUrl: 'template/datepicker/day.html',
1196 require: '^datepicker',
1197 link: function(scope, element, attrs, ctrl) {
1198 scope.showWeeks = ctrl.showWeeks;
1199
1200 ctrl.step = { months: 1 };
1201 ctrl.element = element;
1202
1203 var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1204 function getDaysInMonth( year, month ) {
1205 return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
1206 }
1207
1208 function getDates(startDate, n) {
1209 var dates = new Array(n), current = new Date(startDate), i = 0;
1210 current.setHours(12); // Prevent repeated dates because of timezone bug
1211 while ( i < n ) {
1212 dates[i++] = new Date(current);
1213 current.setDate( current.getDate() + 1 );
1214 }
1215 return dates;
1216 }
1217
1218 ctrl._refreshView = function() {
1219 var year = ctrl.activeDate.getFullYear(),
1220 month = ctrl.activeDate.getMonth(),
1221 firstDayOfMonth = new Date(year, month, 1),
1222 difference = ctrl.startingDay - firstDayOfMonth.getDay(),
1223 numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
1224 firstDate = new Date(firstDayOfMonth);
1225
1226 if ( numDisplayedFromPreviousMonth > 0 ) {
1227 firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
1228 }
1229
1230 // 42 is the number of days on a six-month calendar
1231 var days = getDates(firstDate, 42);
1232 for (var i = 0; i < 42; i ++) {
1233 days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
1234 secondary: days[i].getMonth() !== month,
1235 uid: scope.uniqueId + '-' + i
1236 });
1237 }
1238
1239 scope.labels = new Array(7);
1240 for (var j = 0; j < 7; j++) {
1241 scope.labels[j] = {
1242 abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
1243 full: dateFilter(days[j].date, 'EEEE')
1244 };
1245 }
1246
1247 scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
1248 scope.rows = ctrl.split(days, 7);
1249
1250 if ( scope.showWeeks ) {
1251 scope.weekNumbers = [];
1252 var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ),
1253 numWeeks = scope.rows.length;
1254 while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {}
1255 }
1256 };
1257
1258 ctrl.compare = function(date1, date2) {
1259 return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
1260 };
1261
1262 function getISO8601WeekNumber(date) {
1263 var checkDate = new Date(date);
1264 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
1265 var time = checkDate.getTime();
1266 checkDate.setMonth(0); // Compare with Jan 1
1267 checkDate.setDate(1);
1268 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1269 }
1270
1271 ctrl.handleKeyDown = function( key, evt ) {
1272 var date = ctrl.activeDate.getDate();
1273
1274 if (key === 'left') {
1275 date = date - 1; // up
1276 } else if (key === 'up') {
1277 date = date - 7; // down
1278 } else if (key === 'right') {
1279 date = date + 1; // down
1280 } else if (key === 'down') {
1281 date = date + 7;
1282 } else if (key === 'pageup' || key === 'pagedown') {
1283 var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
1284 ctrl.activeDate.setMonth(month, 1);
1285 date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
1286 } else if (key === 'home') {
1287 date = 1;
1288 } else if (key === 'end') {
1289 date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
1290 }
1291 ctrl.activeDate.setDate(date);
1292 };
1293
1294 ctrl.refreshView();
1295 }
1296 };
1297}])
1298
1299.directive('monthpicker', ['dateFilter', function (dateFilter) {
1300 return {
1301 restrict: 'EA',
1302 replace: true,
1303 templateUrl: 'template/datepicker/month.html',
1304 require: '^datepicker',
1305 link: function(scope, element, attrs, ctrl) {
1306 ctrl.step = { years: 1 };
1307 ctrl.element = element;
1308
1309 ctrl._refreshView = function() {
1310 var months = new Array(12),
1311 year = ctrl.activeDate.getFullYear();
1312
1313 for ( var i = 0; i < 12; i++ ) {
1314 months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), {
1315 uid: scope.uniqueId + '-' + i
1316 });
1317 }
1318
1319 scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
1320 scope.rows = ctrl.split(months, 3);
1321 };
1322
1323 ctrl.compare = function(date1, date2) {
1324 return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
1325 };
1326
1327 ctrl.handleKeyDown = function( key, evt ) {
1328 var date = ctrl.activeDate.getMonth();
1329
1330 if (key === 'left') {
1331 date = date - 1; // up
1332 } else if (key === 'up') {
1333 date = date - 3; // down
1334 } else if (key === 'right') {
1335 date = date + 1; // down
1336 } else if (key === 'down') {
1337 date = date + 3;
1338 } else if (key === 'pageup' || key === 'pagedown') {
1339 var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
1340 ctrl.activeDate.setFullYear(year);
1341 } else if (key === 'home') {
1342 date = 0;
1343 } else if (key === 'end') {
1344 date = 11;
1345 }
1346 ctrl.activeDate.setMonth(date);
1347 };
1348
1349 ctrl.refreshView();
1350 }
1351 };
1352}])
1353
1354.directive('yearpicker', ['dateFilter', function (dateFilter) {
1355 return {
1356 restrict: 'EA',
1357 replace: true,
1358 templateUrl: 'template/datepicker/year.html',
1359 require: '^datepicker',
1360 link: function(scope, element, attrs, ctrl) {
1361 var range = ctrl.yearRange;
1362
1363 ctrl.step = { years: range };
1364 ctrl.element = element;
1365
1366 function getStartingYear( year ) {
1367 return parseInt((year - 1) / range, 10) * range + 1;
1368 }
1369
1370 ctrl._refreshView = function() {
1371 var years = new Array(range);
1372
1373 for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
1374 years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), {
1375 uid: scope.uniqueId + '-' + i
1376 });
1377 }
1378
1379 scope.title = [years[0].label, years[range - 1].label].join(' - ');
1380 scope.rows = ctrl.split(years, 5);
1381 };
1382
1383 ctrl.compare = function(date1, date2) {
1384 return date1.getFullYear() - date2.getFullYear();
1385 };
1386
1387 ctrl.handleKeyDown = function( key, evt ) {
1388 var date = ctrl.activeDate.getFullYear();
1389
1390 if (key === 'left') {
1391 date = date - 1; // up
1392 } else if (key === 'up') {
1393 date = date - 5; // down
1394 } else if (key === 'right') {
1395 date = date + 1; // down
1396 } else if (key === 'down') {
1397 date = date + 5;
1398 } else if (key === 'pageup' || key === 'pagedown') {
1399 date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
1400 } else if (key === 'home') {
1401 date = getStartingYear( ctrl.activeDate.getFullYear() );
1402 } else if (key === 'end') {
1403 date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1;
1404 }
1405 ctrl.activeDate.setFullYear(date);
1406 };
1407
1408 ctrl.refreshView();
1409 }
1410 };
1411}])
1412
1413.constant('datepickerPopupConfig', {
1414 datepickerPopup: 'yyyy-MM-dd',
1415 currentText: 'Today',
1416 clearText: 'Clear',
1417 closeText: 'Done',
1418 closeOnDateSelection: true,
1419 appendToBody: false,
1420 showButtonBar: true
1421})
1422
1423.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig',
1424function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) {
1425 return {
1426 restrict: 'EA',
1427 require: 'ngModel',
1428 scope: {
1429 isOpen: '=?',
1430 currentText: '@',
1431 clearText: '@',
1432 closeText: '@',
1433 dateDisabled: '&'
1434 },
1435 link: function(scope, element, attrs, ngModel) {
1436 var dateFormat,
1437 closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
1438 appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
1439
1440 scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
1441
1442 scope.getText = function( key ) {
1443 return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
1444 };
1445
1446 attrs.$observe('datepickerPopup', function(value) {
1447 dateFormat = value || datepickerPopupConfig.datepickerPopup;
1448 ngModel.$render();
1449 });
1450
1451 // popup element used to display calendar
1452 var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
1453 popupEl.attr({
1454 'ng-model': 'date',
1455 'ng-change': 'dateSelection()'
1456 });
1457
1458 function cameltoDash( string ){
1459 return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
1460 }
1461
1462 // datepicker element
1463 var datepickerEl = angular.element(popupEl.children()[0]);
1464 if ( attrs.datepickerOptions ) {
1465 angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) {
1466 datepickerEl.attr( cameltoDash(option), value );
1467 });
1468 }
1469
1470 scope.watchData = {};
1471 angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) {
1472 if ( attrs[key] ) {
1473 var getAttribute = $parse(attrs[key]);
1474 scope.$parent.$watch(getAttribute, function(value){
1475 scope.watchData[key] = value;
1476 });
1477 datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
1478
1479 // Propagate changes from datepicker to outside
1480 if ( key === 'datepickerMode' ) {
1481 var setAttribute = getAttribute.assign;
1482 scope.$watch('watchData.' + key, function(value, oldvalue) {
1483 if ( value !== oldvalue ) {
1484 setAttribute(scope.$parent, value);
1485 }
1486 });
1487 }
1488 }
1489 });
1490 if (attrs.dateDisabled) {
1491 datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
1492 }
1493
1494 function parseDate(viewValue) {
1495 if (!viewValue) {
1496 ngModel.$setValidity('date', true);
1497 return null;
1498 } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
1499 ngModel.$setValidity('date', true);
1500 return viewValue;
1501 } else if (angular.isString(viewValue)) {
1502 var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
1503 if (isNaN(date)) {
1504 ngModel.$setValidity('date', false);
1505 return undefined;
1506 } else {
1507 ngModel.$setValidity('date', true);
1508 return date;
1509 }
1510 } else {
1511 ngModel.$setValidity('date', false);
1512 return undefined;
1513 }
1514 }
1515 ngModel.$parsers.unshift(parseDate);
1516
1517 // Inner change
1518 scope.dateSelection = function(dt) {
1519 if (angular.isDefined(dt)) {
1520 scope.date = dt;
1521 }
1522 ngModel.$setViewValue(scope.date);
1523 ngModel.$render();
1524
1525 if ( closeOnDateSelection ) {
1526 scope.isOpen = false;
1527 element[0].focus();
1528 }
1529 };
1530
1531 element.bind('input change keyup', function() {
1532 scope.$apply(function() {
1533 scope.date = ngModel.$modelValue;
1534 });
1535 });
1536
1537 // Outter change
1538 ngModel.$render = function() {
1539 var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
1540 element.val(date);
1541 scope.date = parseDate( ngModel.$modelValue );
1542 };
1543
1544 var documentClickBind = function(event) {
1545 if (scope.isOpen && event.target !== element[0]) {
1546 scope.$apply(function() {
1547 scope.isOpen = false;
1548 });
1549 }
1550 };
1551
1552 var keydown = function(evt, noApply) {
1553 scope.keydown(evt);
1554 };
1555 element.bind('keydown', keydown);
1556
1557 scope.keydown = function(evt) {
1558 if (evt.which === 27) {
1559 evt.preventDefault();
1560 evt.stopPropagation();
1561 scope.close();
1562 } else if (evt.which === 40 && !scope.isOpen) {
1563 scope.isOpen = true;
1564 }
1565 };
1566
1567 scope.$watch('isOpen', function(value) {
1568 if (value) {
1569 scope.$broadcast('datepicker.focus');
1570 scope.position = appendToBody ? $position.offset(element) : $position.position(element);
1571 scope.position.top = scope.position.top + element.prop('offsetHeight');
1572
1573 $document.bind('click', documentClickBind);
1574 } else {
1575 $document.unbind('click', documentClickBind);
1576 }
1577 });
1578
1579 scope.select = function( date ) {
1580 if (date === 'today') {
1581 var today = new Date();
1582 if (angular.isDate(ngModel.$modelValue)) {
1583 date = new Date(ngModel.$modelValue);
1584 date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
1585 } else {
1586 date = new Date(today.setHours(0, 0, 0, 0));
1587 }
1588 }
1589 scope.dateSelection( date );
1590 };
1591
1592 scope.close = function() {
1593 scope.isOpen = false;
1594 element[0].focus();
1595 };
1596
1597 var $popup = $compile(popupEl)(scope);
1598 // Prevent jQuery cache memory leak (template is now redundant after linking)
1599 popupEl.remove();
1600
1601 if ( appendToBody ) {
1602 $document.find('body').append($popup);
1603 } else {
1604 element.after($popup);
1605 }
1606
1607 scope.$on('$destroy', function() {
1608 $popup.remove();
1609 element.unbind('keydown', keydown);
1610 $document.unbind('click', documentClickBind);
1611 });
1612 }
1613 };
1614}])
1615
1616.directive('datepickerPopupWrap', function() {
1617 return {
1618 restrict:'EA',
1619 replace: true,
1620 transclude: true,
1621 templateUrl: 'template/datepicker/popup.html',
1622 link:function (scope, element, attrs) {
1623 element.bind('click', function(event) {
1624 event.preventDefault();
1625 event.stopPropagation();
1626 });
1627 }
1628 };
1629});
1630
1631angular.module('ui.bootstrap.dropdown', [])
1632
1633.constant('dropdownConfig', {
1634 openClass: 'open'
1635})
1636
1637.service('dropdownService', ['$document', function($document) {
1638 var openScope = null;
1639
1640 this.open = function( dropdownScope ) {
1641 if ( !openScope ) {
1642 $document.bind('click', closeDropdown);
1643 $document.bind('keydown', escapeKeyBind);
1644 }
1645
1646 if ( openScope && openScope !== dropdownScope ) {
1647 openScope.isOpen = false;
1648 }
1649
1650 openScope = dropdownScope;
1651 };
1652
1653 this.close = function( dropdownScope ) {
1654 if ( openScope === dropdownScope ) {
1655 openScope = null;
1656 $document.unbind('click', closeDropdown);
1657 $document.unbind('keydown', escapeKeyBind);
1658 }
1659 };
1660
1661 var closeDropdown = function( evt ) {
1662 // This method may still be called during the same mouse event that
1663 // unbound this event handler. So check openScope before proceeding.
1664 if (!openScope) { return; }
1665
1666 var toggleElement = openScope.getToggleElement();
1667 if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
1668 return;
1669 }
1670
1671 openScope.$apply(function() {
1672 openScope.isOpen = false;
1673 });
1674 };
1675
1676 var escapeKeyBind = function( evt ) {
1677 if ( evt.which === 27 ) {
1678 openScope.focusToggleElement();
1679 closeDropdown();
1680 }
1681 };
1682}])
1683
1684.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) {
1685 var self = this,
1686 scope = $scope.$new(), // create a child scope so we are not polluting original one
1687 openClass = dropdownConfig.openClass,
1688 getIsOpen,
1689 setIsOpen = angular.noop,
1690 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop;
1691
1692 this.init = function( element ) {
1693 self.$element = element;
1694
1695 if ( $attrs.isOpen ) {
1696 getIsOpen = $parse($attrs.isOpen);
1697 setIsOpen = getIsOpen.assign;
1698
1699 $scope.$watch(getIsOpen, function(value) {
1700 scope.isOpen = !!value;
1701 });
1702 }
1703 };
1704
1705 this.toggle = function( open ) {
1706 return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
1707 };
1708
1709 // Allow other directives to watch status
1710 this.isOpen = function() {
1711 return scope.isOpen;
1712 };
1713
1714 scope.getToggleElement = function() {
1715 return self.toggleElement;
1716 };
1717
1718 scope.focusToggleElement = function() {
1719 if ( self.toggleElement ) {
1720 self.toggleElement[0].focus();
1721 }
1722 };
1723
1724 scope.$watch('isOpen', function( isOpen, wasOpen ) {
1725 $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
1726
1727 if ( isOpen ) {
1728 scope.focusToggleElement();
1729 dropdownService.open( scope );
1730 } else {
1731 dropdownService.close( scope );
1732 }
1733
1734 setIsOpen($scope, isOpen);
1735 if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
1736 toggleInvoker($scope, { open: !!isOpen });
1737 }
1738 });
1739
1740 $scope.$on('$locationChangeSuccess', function() {
1741 scope.isOpen = false;
1742 });
1743
1744 $scope.$on('$destroy', function() {
1745 scope.$destroy();
1746 });
1747}])
1748
1749.directive('dropdown', function() {
1750 return {
1751 controller: 'DropdownController',
1752 link: function(scope, element, attrs, dropdownCtrl) {
1753 dropdownCtrl.init( element );
1754 }
1755 };
1756})
1757
1758.directive('dropdownToggle', function() {
1759 return {
1760 require: '?^dropdown',
1761 link: function(scope, element, attrs, dropdownCtrl) {
1762 if ( !dropdownCtrl ) {
1763 return;
1764 }
1765
1766 dropdownCtrl.toggleElement = element;
1767
1768 var toggleDropdown = function(event) {
1769 event.preventDefault();
1770
1771 if ( !element.hasClass('disabled') && !attrs.disabled ) {
1772 scope.$apply(function() {
1773 dropdownCtrl.toggle();
1774 });
1775 }
1776 };
1777
1778 element.bind('click', toggleDropdown);
1779
1780 // WAI-ARIA
1781 element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
1782 scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
1783 element.attr('aria-expanded', !!isOpen);
1784 });
1785
1786 scope.$on('$destroy', function() {
1787 element.unbind('click', toggleDropdown);
1788 });
1789 }
1790 };
1791});
1792
1793angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1794
1795/**
1796 * A helper, internal data structure that acts as a map but also allows getting / removing
1797 * elements in the LIFO order
1798 */
1799 .factory('$$stackedMap', function () {
1800 return {
1801 createNew: function () {
1802 var stack = [];
1803
1804 return {
1805 add: function (key, value) {
1806 stack.push({
1807 key: key,
1808 value: value
1809 });
1810 },
1811 get: function (key) {
1812 for (var i = 0; i < stack.length; i++) {
1813 if (key == stack[i].key) {
1814 return stack[i];
1815 }
1816 }
1817 },
1818 keys: function() {
1819 var keys = [];
1820 for (var i = 0; i < stack.length; i++) {
1821 keys.push(stack[i].key);
1822 }
1823 return keys;
1824 },
1825 top: function () {
1826 return stack[stack.length - 1];
1827 },
1828 remove: function (key) {
1829 var idx = -1;
1830 for (var i = 0; i < stack.length; i++) {
1831 if (key == stack[i].key) {
1832 idx = i;
1833 break;
1834 }
1835 }
1836 return stack.splice(idx, 1)[0];
1837 },
1838 removeTop: function () {
1839 return stack.splice(stack.length - 1, 1)[0];
1840 },
1841 length: function () {
1842 return stack.length;
1843 }
1844 };
1845 }
1846 };
1847 })
1848
1849/**
1850 * A helper directive for the $modal service. It creates a backdrop element.
1851 */
1852 .directive('modalBackdrop', ['$timeout', function ($timeout) {
1853 return {
1854 restrict: 'EA',
1855 replace: true,
1856 templateUrl: 'template/modal/backdrop.html',
1857 link: function (scope, element, attrs) {
1858 scope.backdropClass = attrs.backdropClass || '';
1859
1860 scope.animate = false;
1861
1862 //trigger CSS transitions
1863 $timeout(function () {
1864 scope.animate = true;
1865 });
1866 }
1867 };
1868 }])
1869
1870 .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
1871 return {
1872 restrict: 'EA',
1873 scope: {
1874 index: '@',
1875 animate: '='
1876 },
1877 replace: true,
1878 transclude: true,
1879 templateUrl: function(tElement, tAttrs) {
1880 return tAttrs.templateUrl || 'template/modal/window.html';
1881 },
1882 link: function (scope, element, attrs) {
1883 element.addClass(attrs.windowClass || '');
1884 scope.size = attrs.size;
1885
1886 $timeout(function () {
1887 // trigger CSS transitions
1888 scope.animate = true;
1889
1890 /**
1891 * Auto-focusing of a freshly-opened modal element causes any child elements
1892 * with the autofocus attribute to lose focus. This is an issue on touch
1893 * based devices which will show and then hide the onscreen keyboard.
1894 * Attempts to refocus the autofocus element via JavaScript will not reopen
1895 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
1896 * the modal element if the modal does not contain an autofocus element.
1897 */
1898 if (!element[0].querySelectorAll('[autofocus]').length) {
1899 element[0].focus();
1900 }
1901 });
1902
1903 scope.close = function (evt) {
1904 var modal = $modalStack.getTop();
1905 if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
1906 evt.preventDefault();
1907 evt.stopPropagation();
1908 $modalStack.dismiss(modal.key, 'backdrop click');
1909 }
1910 };
1911 }
1912 };
1913 }])
1914
1915 .directive('modalTransclude', function () {
1916 return {
1917 link: function($scope, $element, $attrs, controller, $transclude) {
1918 $transclude($scope.$parent, function(clone) {
1919 $element.empty();
1920 $element.append(clone);
1921 });
1922 }
1923 };
1924 })
1925
1926 .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
1927 function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
1928
1929 var OPENED_MODAL_CLASS = 'modal-open';
1930
1931 var backdropDomEl, backdropScope;
1932 var openedWindows = $$stackedMap.createNew();
1933 var $modalStack = {};
1934
1935 function backdropIndex() {
1936 var topBackdropIndex = -1;
1937 var opened = openedWindows.keys();
1938 for (var i = 0; i < opened.length; i++) {
1939 if (openedWindows.get(opened[i]).value.backdrop) {
1940 topBackdropIndex = i;
1941 }
1942 }
1943 return topBackdropIndex;
1944 }
1945
1946 $rootScope.$watch(backdropIndex, function(newBackdropIndex){
1947 if (backdropScope) {
1948 backdropScope.index = newBackdropIndex;
1949 }
1950 });
1951
1952 function removeModalWindow(modalInstance) {
1953
1954 var body = $document.find('body').eq(0);
1955 var modalWindow = openedWindows.get(modalInstance).value;
1956
1957 //clean up the stack
1958 openedWindows.remove(modalInstance);
1959
1960 //remove window DOM element
1961 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
1962 modalWindow.modalScope.$destroy();
1963 body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
1964 checkRemoveBackdrop();
1965 });
1966 }
1967
1968 function checkRemoveBackdrop() {
1969 //remove backdrop if no longer needed
1970 if (backdropDomEl && backdropIndex() == -1) {
1971 var backdropScopeRef = backdropScope;
1972 removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
1973 backdropScopeRef.$destroy();
1974 backdropScopeRef = null;
1975 });
1976 backdropDomEl = undefined;
1977 backdropScope = undefined;
1978 }
1979 }
1980
1981 function removeAfterAnimate(domEl, scope, emulateTime, done) {
1982 // Closing animation
1983 scope.animate = false;
1984
1985 var transitionEndEventName = $transition.transitionEndEventName;
1986 if (transitionEndEventName) {
1987 // transition out
1988 var timeout = $timeout(afterAnimating, emulateTime);
1989
1990 domEl.bind(transitionEndEventName, function () {
1991 $timeout.cancel(timeout);
1992 afterAnimating();
1993 scope.$apply();
1994 });
1995 } else {
1996 // Ensure this call is async
1997 $timeout(afterAnimating);
1998 }
1999
2000 function afterAnimating() {
2001 if (afterAnimating.done) {
2002 return;
2003 }
2004 afterAnimating.done = true;
2005
2006 domEl.remove();
2007 if (done) {
2008 done();
2009 }
2010 }
2011 }
2012
2013 $document.bind('keydown', function (evt) {
2014 var modal;
2015
2016 if (evt.which === 27) {
2017 modal = openedWindows.top();
2018 if (modal && modal.value.keyboard) {
2019 evt.preventDefault();
2020 $rootScope.$apply(function () {
2021 $modalStack.dismiss(modal.key, 'escape key press');
2022 });
2023 }
2024 }
2025 });
2026
2027 $modalStack.open = function (modalInstance, modal) {
2028
2029 openedWindows.add(modalInstance, {
2030 deferred: modal.deferred,
2031 modalScope: modal.scope,
2032 backdrop: modal.backdrop,
2033 keyboard: modal.keyboard
2034 });
2035
2036 var body = $document.find('body').eq(0),
2037 currBackdropIndex = backdropIndex();
2038
2039 if (currBackdropIndex >= 0 && !backdropDomEl) {
2040 backdropScope = $rootScope.$new(true);
2041 backdropScope.index = currBackdropIndex;
2042 var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
2043 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
2044 backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
2045 body.append(backdropDomEl);
2046 }
2047
2048 var angularDomEl = angular.element('<div modal-window></div>');
2049 angularDomEl.attr({
2050 'template-url': modal.windowTemplateUrl,
2051 'window-class': modal.windowClass,
2052 'size': modal.size,
2053 'index': openedWindows.length() - 1,
2054 'animate': 'animate'
2055 }).html(modal.content);
2056
2057 var modalDomEl = $compile(angularDomEl)(modal.scope);
2058 openedWindows.top().value.modalDomEl = modalDomEl;
2059 body.append(modalDomEl);
2060 body.addClass(OPENED_MODAL_CLASS);
2061 };
2062
2063 $modalStack.close = function (modalInstance, result) {
2064 var modalWindow = openedWindows.get(modalInstance);
2065 if (modalWindow) {
2066 modalWindow.value.deferred.resolve(result);
2067 removeModalWindow(modalInstance);
2068 }
2069 };
2070
2071 $modalStack.dismiss = function (modalInstance, reason) {
2072 var modalWindow = openedWindows.get(modalInstance);
2073 if (modalWindow) {
2074 modalWindow.value.deferred.reject(reason);
2075 removeModalWindow(modalInstance);
2076 }
2077 };
2078
2079 $modalStack.dismissAll = function (reason) {
2080 var topModal = this.getTop();
2081 while (topModal) {
2082 this.dismiss(topModal.key, reason);
2083 topModal = this.getTop();
2084 }
2085 };
2086
2087 $modalStack.getTop = function () {
2088 return openedWindows.top();
2089 };
2090
2091 return $modalStack;
2092 }])
2093
2094 .provider('$modal', function () {
2095
2096 var $modalProvider = {
2097 options: {
2098 backdrop: true, //can be also false or 'static'
2099 keyboard: true
2100 },
2101 $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
2102 function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
2103
2104 var $modal = {};
2105
2106 function getTemplatePromise(options) {
2107 return options.template ? $q.when(options.template) :
2108 $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl,
2109 {cache: $templateCache}).then(function (result) {
2110 return result.data;
2111 });
2112 }
2113
2114 function getResolvePromises(resolves) {
2115 var promisesArr = [];
2116 angular.forEach(resolves, function (value) {
2117 if (angular.isFunction(value) || angular.isArray(value)) {
2118 promisesArr.push($q.when($injector.invoke(value)));
2119 }
2120 });
2121 return promisesArr;
2122 }
2123
2124 $modal.open = function (modalOptions) {
2125
2126 var modalResultDeferred = $q.defer();
2127 var modalOpenedDeferred = $q.defer();
2128
2129 //prepare an instance of a modal to be injected into controllers and returned to a caller
2130 var modalInstance = {
2131 result: modalResultDeferred.promise,
2132 opened: modalOpenedDeferred.promise,
2133 close: function (result) {
2134 $modalStack.close(modalInstance, result);
2135 },
2136 dismiss: function (reason) {
2137 $modalStack.dismiss(modalInstance, reason);
2138 }
2139 };
2140
2141 //merge and clean up options
2142 modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
2143 modalOptions.resolve = modalOptions.resolve || {};
2144
2145 //verify options
2146 if (!modalOptions.template && !modalOptions.templateUrl) {
2147 throw new Error('One of template or templateUrl options is required.');
2148 }
2149
2150 var templateAndResolvePromise =
2151 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
2152
2153
2154 templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
2155
2156 var modalScope = (modalOptions.scope || $rootScope).$new();
2157 modalScope.$close = modalInstance.close;
2158 modalScope.$dismiss = modalInstance.dismiss;
2159
2160 var ctrlInstance, ctrlLocals = {};
2161 var resolveIter = 1;
2162
2163 //controllers
2164 if (modalOptions.controller) {
2165 ctrlLocals.$scope = modalScope;
2166 ctrlLocals.$modalInstance = modalInstance;
2167 angular.forEach(modalOptions.resolve, function (value, key) {
2168 ctrlLocals[key] = tplAndVars[resolveIter++];
2169 });
2170
2171 ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
2172 if (modalOptions.controllerAs) {
2173 modalScope[modalOptions.controllerAs] = ctrlInstance;
2174 }
2175 }
2176
2177 $modalStack.open(modalInstance, {
2178 scope: modalScope,
2179 deferred: modalResultDeferred,
2180 content: tplAndVars[0],
2181 backdrop: modalOptions.backdrop,
2182 keyboard: modalOptions.keyboard,
2183 backdropClass: modalOptions.backdropClass,
2184 windowClass: modalOptions.windowClass,
2185 windowTemplateUrl: modalOptions.windowTemplateUrl,
2186 size: modalOptions.size
2187 });
2188
2189 }, function resolveError(reason) {
2190 modalResultDeferred.reject(reason);
2191 });
2192
2193 templateAndResolvePromise.then(function () {
2194 modalOpenedDeferred.resolve(true);
2195 }, function () {
2196 modalOpenedDeferred.reject(false);
2197 });
2198
2199 return modalInstance;
2200 };
2201
2202 return $modal;
2203 }]
2204 };
2205
2206 return $modalProvider;
2207 });
2208
2209angular.module('ui.bootstrap.pagination', [])
2210
2211.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
2212 var self = this,
2213 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
2214 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
2215
2216 this.init = function(ngModelCtrl_, config) {
2217 ngModelCtrl = ngModelCtrl_;
2218 this.config = config;
2219
2220 ngModelCtrl.$render = function() {
2221 self.render();
2222 };
2223
2224 if ($attrs.itemsPerPage) {
2225 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
2226 self.itemsPerPage = parseInt(value, 10);
2227 $scope.totalPages = self.calculateTotalPages();
2228 });
2229 } else {
2230 this.itemsPerPage = config.itemsPerPage;
2231 }
2232 };
2233
2234 this.calculateTotalPages = function() {
2235 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
2236 return Math.max(totalPages || 0, 1);
2237 };
2238
2239 this.render = function() {
2240 $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
2241 };
2242
2243 $scope.selectPage = function(page) {
2244 if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) {
2245 ngModelCtrl.$setViewValue(page);
2246 ngModelCtrl.$render();
2247 }
2248 };
2249
2250 $scope.getText = function( key ) {
2251 return $scope[key + 'Text'] || self.config[key + 'Text'];
2252 };
2253 $scope.noPrevious = function() {
2254 return $scope.page === 1;
2255 };
2256 $scope.noNext = function() {
2257 return $scope.page === $scope.totalPages;
2258 };
2259
2260 $scope.$watch('totalItems', function() {
2261 $scope.totalPages = self.calculateTotalPages();
2262 });
2263
2264 $scope.$watch('totalPages', function(value) {
2265 setNumPages($scope.$parent, value); // Readonly variable
2266
2267 if ( $scope.page > value ) {
2268 $scope.selectPage(value);
2269 } else {
2270 ngModelCtrl.$render();
2271 }
2272 });
2273}])
2274
2275.constant('paginationConfig', {
2276 itemsPerPage: 10,
2277 boundaryLinks: false,
2278 directionLinks: true,
2279 firstText: 'First',
2280 previousText: 'Previous',
2281 nextText: 'Next',
2282 lastText: 'Last',
2283 rotate: true
2284})
2285
2286.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) {
2287 return {
2288 restrict: 'EA',
2289 scope: {
2290 totalItems: '=',
2291 firstText: '@',
2292 previousText: '@',
2293 nextText: '@',
2294 lastText: '@'
2295 },
2296 require: ['pagination', '?ngModel'],
2297 controller: 'PaginationController',
2298 templateUrl: 'template/pagination/pagination.html',
2299 replace: true,
2300 link: function(scope, element, attrs, ctrls) {
2301 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2302
2303 if (!ngModelCtrl) {
2304 return; // do nothing if no ng-model
2305 }
2306
2307 // Setup configuration parameters
2308 var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
2309 rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
2310 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
2311 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
2312
2313 paginationCtrl.init(ngModelCtrl, paginationConfig);
2314
2315 if (attrs.maxSize) {
2316 scope.$parent.$watch($parse(attrs.maxSize), function(value) {
2317 maxSize = parseInt(value, 10);
2318 paginationCtrl.render();
2319 });
2320 }
2321
2322 // Create page object used in template
2323 function makePage(number, text, isActive) {
2324 return {
2325 number: number,
2326 text: text,
2327 active: isActive
2328 };
2329 }
2330
2331 function getPages(currentPage, totalPages) {
2332 var pages = [];
2333
2334 // Default page limits
2335 var startPage = 1, endPage = totalPages;
2336 var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );
2337
2338 // recompute if maxSize
2339 if ( isMaxSized ) {
2340 if ( rotate ) {
2341 // Current page is displayed in the middle of the visible ones
2342 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
2343 endPage = startPage + maxSize - 1;
2344
2345 // Adjust if limit is exceeded
2346 if (endPage > totalPages) {
2347 endPage = totalPages;
2348 startPage = endPage - maxSize + 1;
2349 }
2350 } else {
2351 // Visible pages are paginated with maxSize
2352 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
2353
2354 // Adjust last page if limit is exceeded
2355 endPage = Math.min(startPage + maxSize - 1, totalPages);
2356 }
2357 }
2358
2359 // Add page number links
2360 for (var number = startPage; number <= endPage; number++) {
2361 var page = makePage(number, number, number === currentPage);
2362 pages.push(page);
2363 }
2364
2365 // Add links to move between page sets
2366 if ( isMaxSized && ! rotate ) {
2367 if ( startPage > 1 ) {
2368 var previousPageSet = makePage(startPage - 1, '...', false);
2369 pages.unshift(previousPageSet);
2370 }
2371
2372 if ( endPage < totalPages ) {
2373 var nextPageSet = makePage(endPage + 1, '...', false);
2374 pages.push(nextPageSet);
2375 }
2376 }
2377
2378 return pages;
2379 }
2380
2381 var originalRender = paginationCtrl.render;
2382 paginationCtrl.render = function() {
2383 originalRender();
2384 if (scope.page > 0 && scope.page <= scope.totalPages) {
2385 scope.pages = getPages(scope.page, scope.totalPages);
2386 }
2387 };
2388 }
2389 };
2390}])
2391
2392.constant('pagerConfig', {
2393 itemsPerPage: 10,
2394 previousText: '« Previous',
2395 nextText: 'Next »',
2396 align: true
2397})
2398
2399.directive('pager', ['pagerConfig', function(pagerConfig) {
2400 return {
2401 restrict: 'EA',
2402 scope: {
2403 totalItems: '=',
2404 previousText: '@',
2405 nextText: '@'
2406 },
2407 require: ['pager', '?ngModel'],
2408 controller: 'PaginationController',
2409 templateUrl: 'template/pagination/pager.html',
2410 replace: true,
2411 link: function(scope, element, attrs, ctrls) {
2412 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2413
2414 if (!ngModelCtrl) {
2415 return; // do nothing if no ng-model
2416 }
2417
2418 scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
2419 paginationCtrl.init(ngModelCtrl, pagerConfig);
2420 }
2421 };
2422}]);
2423
2424/**
2425 * The following features are still outstanding: animation as a
2426 * function, placement as a function, inside, support for more triggers than
2427 * just mouse enter/leave, html tooltips, and selector delegation.
2428 */
2429angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] )
2430
2431/**
2432 * The $tooltip service creates tooltip- and popover-like directives as well as
2433 * houses global options for them.
2434 */
2435.provider( '$tooltip', function () {
2436 // The default options tooltip and popover.
2437 var defaultOptions = {
2438 placement: 'top',
2439 animation: true,
2440 popupDelay: 0
2441 };
2442
2443 // Default hide triggers for each show trigger
2444 var triggerMap = {
2445 'mouseenter': 'mouseleave',
2446 'click': 'click',
2447 'focus': 'blur'
2448 };
2449
2450 // The options specified to the provider globally.
2451 var globalOptions = {};
2452
2453 /**
2454 * `options({})` allows global configuration of all tooltips in the
2455 * application.
2456 *
2457 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
2458 * // place tooltips left instead of top by default
2459 * $tooltipProvider.options( { placement: 'left' } );
2460 * });
2461 */
2462 this.options = function( value ) {
2463 angular.extend( globalOptions, value );
2464 };
2465
2466 /**
2467 * This allows you to extend the set of trigger mappings available. E.g.:
2468 *
2469 * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
2470 */
2471 this.setTriggers = function setTriggers ( triggers ) {
2472 angular.extend( triggerMap, triggers );
2473 };
2474
2475 /**
2476 * This is a helper function for translating camel-case to snake-case.
2477 */
2478 function snake_case(name){
2479 var regexp = /[A-Z]/g;
2480 var separator = '-';
2481 return name.replace(regexp, function(letter, pos) {
2482 return (pos ? separator : '') + letter.toLowerCase();
2483 });
2484 }
2485
2486 /**
2487 * Returns the actual instance of the $tooltip service.
2488 * TODO support multiple triggers
2489 */
2490 this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) {
2491 return function $tooltip ( type, prefix, defaultTriggerShow ) {
2492 var options = angular.extend( {}, defaultOptions, globalOptions );
2493
2494 /**
2495 * Returns an object of show and hide triggers.
2496 *
2497 * If a trigger is supplied,
2498 * it is used to show the tooltip; otherwise, it will use the `trigger`
2499 * option passed to the `$tooltipProvider.options` method; else it will
2500 * default to the trigger supplied to this directive factory.
2501 *
2502 * The hide trigger is based on the show trigger. If the `trigger` option
2503 * was passed to the `$tooltipProvider.options` method, it will use the
2504 * mapped trigger from `triggerMap` or the passed trigger if the map is
2505 * undefined; otherwise, it uses the `triggerMap` value of the show
2506 * trigger; else it will just use the show trigger.
2507 */
2508 function getTriggers ( trigger ) {
2509 var show = trigger || options.trigger || defaultTriggerShow;
2510 var hide = triggerMap[show] || show;
2511 return {
2512 show: show,
2513 hide: hide
2514 };
2515 }
2516
2517 var directiveName = snake_case( type );
2518
2519 var startSym = $interpolate.startSymbol();
2520 var endSym = $interpolate.endSymbol();
2521 var template =
2522 '<div '+ directiveName +'-popup '+
2523 'title="'+startSym+'title'+endSym+'" '+
2524 'content="'+startSym+'content'+endSym+'" '+
2525 'placement="'+startSym+'placement'+endSym+'" '+
2526 'animation="animation" '+
2527 'is-open="isOpen"'+
2528 '>'+
2529 '</div>';
2530
2531 return {
2532 restrict: 'EA',
2533 compile: function (tElem, tAttrs) {
2534 var tooltipLinker = $compile( template );
2535
2536 return function link ( scope, element, attrs ) {
2537 var tooltip;
2538 var tooltipLinkedScope;
2539 var transitionTimeout;
2540 var popupTimeout;
2541 var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
2542 var triggers = getTriggers( undefined );
2543 var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
2544 var ttScope = scope.$new(true);
2545
2546 var positionTooltip = function () {
2547
2548 var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
2549 ttPosition.top += 'px';
2550 ttPosition.left += 'px';
2551
2552 // Now set the calculated positioning.
2553 tooltip.css( ttPosition );
2554 };
2555
2556 // By default, the tooltip is not open.
2557 // TODO add ability to start tooltip opened
2558 ttScope.isOpen = false;
2559
2560 function toggleTooltipBind () {
2561 if ( ! ttScope.isOpen ) {
2562 showTooltipBind();
2563 } else {
2564 hideTooltipBind();
2565 }
2566 }
2567
2568 // Show the tooltip with delay if specified, otherwise show it immediately
2569 function showTooltipBind() {
2570 if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) {
2571 return;
2572 }
2573
2574 prepareTooltip();
2575
2576 if ( ttScope.popupDelay ) {
2577 // Do nothing if the tooltip was already scheduled to pop-up.
2578 // This happens if show is triggered multiple times before any hide is triggered.
2579 if (!popupTimeout) {
2580 popupTimeout = $timeout( show, ttScope.popupDelay, false );
2581 popupTimeout.then(function(reposition){reposition();});
2582 }
2583 } else {
2584 show()();
2585 }
2586 }
2587
2588 function hideTooltipBind () {
2589 scope.$apply(function () {
2590 hide();
2591 });
2592 }
2593
2594 // Show the tooltip popup element.
2595 function show() {
2596
2597 popupTimeout = null;
2598
2599 // If there is a pending remove transition, we must cancel it, lest the
2600 // tooltip be mysteriously removed.
2601 if ( transitionTimeout ) {
2602 $timeout.cancel( transitionTimeout );
2603 transitionTimeout = null;
2604 }
2605
2606 // Don't show empty tooltips.
2607 if ( ! ttScope.content ) {
2608 return angular.noop;
2609 }
2610
2611 createTooltip();
2612
2613 // Set the initial positioning.
2614 tooltip.css({ top: 0, left: 0, display: 'block' });
2615
2616 // Now we add it to the DOM because need some info about it. But it's not
2617 // visible yet anyway.
2618 if ( appendToBody ) {
2619 $document.find( 'body' ).append( tooltip );
2620 } else {
2621 element.after( tooltip );
2622 }
2623
2624 positionTooltip();
2625
2626 // And show the tooltip.
2627 ttScope.isOpen = true;
2628 ttScope.$digest(); // digest required as $apply is not called
2629
2630 // Return positioning function as promise callback for correct
2631 // positioning after draw.
2632 return positionTooltip;
2633 }
2634
2635 // Hide the tooltip popup element.
2636 function hide() {
2637 // First things first: we don't show it anymore.
2638 ttScope.isOpen = false;
2639
2640 //if tooltip is going to be shown after delay, we must cancel this
2641 $timeout.cancel( popupTimeout );
2642 popupTimeout = null;
2643
2644 // And now we remove it from the DOM. However, if we have animation, we
2645 // need to wait for it to expire beforehand.
2646 // FIXME: this is a placeholder for a port of the transitions library.
2647 if ( ttScope.animation ) {
2648 if (!transitionTimeout) {
2649 transitionTimeout = $timeout(removeTooltip, 500);
2650 }
2651 } else {
2652 removeTooltip();
2653 }
2654 }
2655
2656 function createTooltip() {
2657 // There can only be one tooltip element per directive shown at once.
2658 if (tooltip) {
2659 removeTooltip();
2660 }
2661 tooltipLinkedScope = ttScope.$new();
2662 tooltip = tooltipLinker(tooltipLinkedScope, angular.noop);
2663 }
2664
2665 function removeTooltip() {
2666 transitionTimeout = null;
2667 if (tooltip) {
2668 tooltip.remove();
2669 tooltip = null;
2670 }
2671 if (tooltipLinkedScope) {
2672 tooltipLinkedScope.$destroy();
2673 tooltipLinkedScope = null;
2674 }
2675 }
2676
2677 function prepareTooltip() {
2678 prepPlacement();
2679 prepPopupDelay();
2680 }
2681
2682 /**
2683 * Observe the relevant attributes.
2684 */
2685 attrs.$observe( type, function ( val ) {
2686 ttScope.content = val;
2687
2688 if (!val && ttScope.isOpen ) {
2689 hide();
2690 }
2691 });
2692
2693 attrs.$observe( prefix+'Title', function ( val ) {
2694 ttScope.title = val;
2695 });
2696
2697 function prepPlacement() {
2698 var val = attrs[ prefix + 'Placement' ];
2699 ttScope.placement = angular.isDefined( val ) ? val : options.placement;
2700 }
2701
2702 function prepPopupDelay() {
2703 var val = attrs[ prefix + 'PopupDelay' ];
2704 var delay = parseInt( val, 10 );
2705 ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
2706 }
2707
2708 var unregisterTriggers = function () {
2709 element.unbind(triggers.show, showTooltipBind);
2710 element.unbind(triggers.hide, hideTooltipBind);
2711 };
2712
2713 function prepTriggers() {
2714 var val = attrs[ prefix + 'Trigger' ];
2715 unregisterTriggers();
2716
2717 triggers = getTriggers( val );
2718
2719 if ( triggers.show === triggers.hide ) {
2720 element.bind( triggers.show, toggleTooltipBind );
2721 } else {
2722 element.bind( triggers.show, showTooltipBind );
2723 element.bind( triggers.hide, hideTooltipBind );
2724 }
2725 }
2726 prepTriggers();
2727
2728 var animation = scope.$eval(attrs[prefix + 'Animation']);
2729 ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
2730
2731 var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
2732 appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
2733
2734 // if a tooltip is attached to <body> we need to remove it on
2735 // location change as its parent scope will probably not be destroyed
2736 // by the change.
2737 if ( appendToBody ) {
2738 scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
2739 if ( ttScope.isOpen ) {
2740 hide();
2741 }
2742 });
2743 }
2744
2745 // Make sure tooltip is destroyed and removed.
2746 scope.$on('$destroy', function onDestroyTooltip() {
2747 $timeout.cancel( transitionTimeout );
2748 $timeout.cancel( popupTimeout );
2749 unregisterTriggers();
2750 removeTooltip();
2751 ttScope = null;
2752 });
2753 };
2754 }
2755 };
2756 };
2757 }];
2758})
2759
2760.directive( 'tooltipPopup', function () {
2761 return {
2762 restrict: 'EA',
2763 replace: true,
2764 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
2765 templateUrl: 'template/tooltip/tooltip-popup.html'
2766 };
2767})
2768
2769.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
2770 return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
2771}])
2772
2773.directive( 'tooltipHtmlUnsafePopup', function () {
2774 return {
2775 restrict: 'EA',
2776 replace: true,
2777 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
2778 templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
2779 };
2780})
2781
2782.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
2783 return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
2784}]);
2785
2786/**
2787 * The following features are still outstanding: popup delay, animation as a
2788 * function, placement as a function, inside, support for more triggers than
2789 * just mouse enter/leave, html popovers, and selector delegatation.
2790 */
2791angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
2792
2793.directive( 'popoverPopup', function () {
2794 return {
2795 restrict: 'EA',
2796 replace: true,
2797 scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
2798 templateUrl: 'template/popover/popover.html'
2799 };
2800})
2801
2802.directive( 'popover', [ '$tooltip', function ( $tooltip ) {
2803 return $tooltip( 'popover', 'popover', 'click' );
2804}]);
2805
2806angular.module('ui.bootstrap.progressbar', [])
2807
2808.constant('progressConfig', {
2809 animate: true,
2810 max: 100
2811})
2812
2813.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
2814 var self = this,
2815 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
2816
2817 this.bars = [];
2818 $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max;
2819
2820 this.addBar = function(bar, element) {
2821 if ( !animate ) {
2822 element.css({'transition': 'none'});
2823 }
2824
2825 this.bars.push(bar);
2826
2827 bar.$watch('value', function( value ) {
2828 bar.percent = +(100 * value / $scope.max).toFixed(2);
2829 });
2830
2831 bar.$on('$destroy', function() {
2832 element = null;
2833 self.removeBar(bar);
2834 });
2835 };
2836
2837 this.removeBar = function(bar) {
2838 this.bars.splice(this.bars.indexOf(bar), 1);
2839 };
2840}])
2841
2842.directive('progress', function() {
2843 return {
2844 restrict: 'EA',
2845 replace: true,
2846 transclude: true,
2847 controller: 'ProgressController',
2848 require: 'progress',
2849 scope: {},
2850 templateUrl: 'template/progressbar/progress.html'
2851 };
2852})
2853
2854.directive('bar', function() {
2855 return {
2856 restrict: 'EA',
2857 replace: true,
2858 transclude: true,
2859 require: '^progress',
2860 scope: {
2861 value: '=',
2862 type: '@'
2863 },
2864 templateUrl: 'template/progressbar/bar.html',
2865 link: function(scope, element, attrs, progressCtrl) {
2866 progressCtrl.addBar(scope, element);
2867 }
2868 };
2869})
2870
2871.directive('progressbar', function() {
2872 return {
2873 restrict: 'EA',
2874 replace: true,
2875 transclude: true,
2876 controller: 'ProgressController',
2877 scope: {
2878 value: '=',
2879 type: '@'
2880 },
2881 templateUrl: 'template/progressbar/progressbar.html',
2882 link: function(scope, element, attrs, progressCtrl) {
2883 progressCtrl.addBar(scope, angular.element(element.children()[0]));
2884 }
2885 };
2886});
2887angular.module('ui.bootstrap.rating', [])
2888
2889.constant('ratingConfig', {
2890 max: 5,
2891 stateOn: null,
2892 stateOff: null
2893})
2894
2895.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
2896 var ngModelCtrl = { $setViewValue: angular.noop };
2897
2898 this.init = function(ngModelCtrl_) {
2899 ngModelCtrl = ngModelCtrl_;
2900 ngModelCtrl.$render = this.render;
2901
2902 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
2903 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
2904
2905 var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
2906 new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
2907 $scope.range = this.buildTemplateObjects(ratingStates);
2908 };
2909
2910 this.buildTemplateObjects = function(states) {
2911 for (var i = 0, n = states.length; i < n; i++) {
2912 states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
2913 }
2914 return states;
2915 };
2916
2917 $scope.rate = function(value) {
2918 if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
2919 ngModelCtrl.$setViewValue(value);
2920 ngModelCtrl.$render();
2921 }
2922 };
2923
2924 $scope.enter = function(value) {
2925 if ( !$scope.readonly ) {
2926 $scope.value = value;
2927 }
2928 $scope.onHover({value: value});
2929 };
2930
2931 $scope.reset = function() {
2932 $scope.value = ngModelCtrl.$viewValue;
2933 $scope.onLeave();
2934 };
2935
2936 $scope.onKeydown = function(evt) {
2937 if (/(37|38|39|40)/.test(evt.which)) {
2938 evt.preventDefault();
2939 evt.stopPropagation();
2940 $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
2941 }
2942 };
2943
2944 this.render = function() {
2945 $scope.value = ngModelCtrl.$viewValue;
2946 };
2947}])
2948
2949.directive('rating', function() {
2950 return {
2951 restrict: 'EA',
2952 require: ['rating', 'ngModel'],
2953 scope: {
2954 readonly: '=?',
2955 onHover: '&',
2956 onLeave: '&'
2957 },
2958 controller: 'RatingController',
2959 templateUrl: 'template/rating/rating.html',
2960 replace: true,
2961 link: function(scope, element, attrs, ctrls) {
2962 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2963
2964 if ( ngModelCtrl ) {
2965 ratingCtrl.init( ngModelCtrl );
2966 }
2967 }
2968 };
2969});
2970
2971/**
2972 * @ngdoc overview
2973 * @name ui.bootstrap.tabs
2974 *
2975 * @description
2976 * AngularJS version of the tabs directive.
2977 */
2978
2979angular.module('ui.bootstrap.tabs', [])
2980
2981.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
2982 var ctrl = this,
2983 tabs = ctrl.tabs = $scope.tabs = [];
2984
2985 ctrl.select = function(selectedTab) {
2986 angular.forEach(tabs, function(tab) {
2987 if (tab.active && tab !== selectedTab) {
2988 tab.active = false;
2989 tab.onDeselect();
2990 }
2991 });
2992 selectedTab.active = true;
2993 selectedTab.onSelect();
2994 };
2995
2996 ctrl.addTab = function addTab(tab) {
2997 tabs.push(tab);
2998 // we can't run the select function on the first tab
2999 // since that would select it twice
3000 if (tabs.length === 1) {
3001 tab.active = true;
3002 } else if (tab.active) {
3003 ctrl.select(tab);
3004 }
3005 };
3006
3007 ctrl.removeTab = function removeTab(tab) {
3008 var index = tabs.indexOf(tab);
3009 //Select a new tab if the tab to be removed is selected and not destroyed
3010 if (tab.active && tabs.length > 1 && !destroyed) {
3011 //If this is the last tab, select the previous tab. else, the next tab.
3012 var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
3013 ctrl.select(tabs[newActiveIndex]);
3014 }
3015 tabs.splice(index, 1);
3016 };
3017
3018 var destroyed;
3019 $scope.$on('$destroy', function() {
3020 destroyed = true;
3021 });
3022}])
3023
3024/**
3025 * @ngdoc directive
3026 * @name ui.bootstrap.tabs.directive:tabset
3027 * @restrict EA
3028 *
3029 * @description
3030 * Tabset is the outer container for the tabs directive
3031 *
3032 * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
3033 * @param {boolean=} justified Whether or not to use justified styling for the tabs.
3034 *
3035 * @example
3036<example module="ui.bootstrap">
3037 <file name="index.html">
3038 <tabset>
3039 <tab heading="Tab 1"><b>First</b> Content!</tab>
3040 <tab heading="Tab 2"><i>Second</i> Content!</tab>
3041 </tabset>
3042 <hr />
3043 <tabset vertical="true">
3044 <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
3045 <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
3046 </tabset>
3047 <tabset justified="true">
3048 <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
3049 <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
3050 </tabset>
3051 </file>
3052</example>
3053 */
3054.directive('tabset', function() {
3055 return {
3056 restrict: 'EA',
3057 transclude: true,
3058 replace: true,
3059 scope: {
3060 type: '@'
3061 },
3062 controller: 'TabsetController',
3063 templateUrl: 'template/tabs/tabset.html',
3064 link: function(scope, element, attrs) {
3065 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
3066 scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
3067 }
3068 };
3069})
3070
3071/**
3072 * @ngdoc directive
3073 * @name ui.bootstrap.tabs.directive:tab
3074 * @restrict EA
3075 *
3076 * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
3077 * @param {string=} select An expression to evaluate when the tab is selected.
3078 * @param {boolean=} active A binding, telling whether or not this tab is selected.
3079 * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
3080 *
3081 * @description
3082 * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
3083 *
3084 * @example
3085<example module="ui.bootstrap">
3086 <file name="index.html">
3087 <div ng-controller="TabsDemoCtrl">
3088 <button class="btn btn-small" ng-click="items[0].active = true">
3089 Select item 1, using active binding
3090 </button>
3091 <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
3092 Enable/disable item 2, using disabled binding
3093 </button>
3094 <br />
3095 <tabset>
3096 <tab heading="Tab 1">First Tab</tab>
3097 <tab select="alertMe()">
3098 <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
3099 Second Tab, with alert callback and html heading!
3100 </tab>
3101 <tab ng-repeat="item in items"
3102 heading="{{item.title}}"
3103 disabled="item.disabled"
3104 active="item.active">
3105 {{item.content}}
3106 </tab>
3107 </tabset>
3108 </div>
3109 </file>
3110 <file name="script.js">
3111 function TabsDemoCtrl($scope) {
3112 $scope.items = [
3113 { title:"Dynamic Title 1", content:"Dynamic Item 0" },
3114 { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
3115 ];
3116
3117 $scope.alertMe = function() {
3118 setTimeout(function() {
3119 alert("You've selected the alert tab!");
3120 });
3121 };
3122 };
3123 </file>
3124</example>
3125 */
3126
3127/**
3128 * @ngdoc directive
3129 * @name ui.bootstrap.tabs.directive:tabHeading
3130 * @restrict EA
3131 *
3132 * @description
3133 * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
3134 *
3135 * @example
3136<example module="ui.bootstrap">
3137 <file name="index.html">
3138 <tabset>
3139 <tab>
3140 <tab-heading><b>HTML</b> in my titles?!</tab-heading>
3141 And some content, too!
3142 </tab>
3143 <tab>
3144 <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
3145 That's right.
3146 </tab>
3147 </tabset>
3148 </file>
3149</example>
3150 */
3151.directive('tab', ['$parse', function($parse) {
3152 return {
3153 require: '^tabset',
3154 restrict: 'EA',
3155 replace: true,
3156 templateUrl: 'template/tabs/tab.html',
3157 transclude: true,
3158 scope: {
3159 active: '=?',
3160 heading: '@',
3161 onSelect: '&select', //This callback is called in contentHeadingTransclude
3162 //once it inserts the tab's content into the dom
3163 onDeselect: '&deselect'
3164 },
3165 controller: function() {
3166 //Empty controller so other directives can require being 'under' a tab
3167 },
3168 compile: function(elm, attrs, transclude) {
3169 return function postLink(scope, elm, attrs, tabsetCtrl) {
3170 scope.$watch('active', function(active) {
3171 if (active) {
3172 tabsetCtrl.select(scope);
3173 }
3174 });
3175
3176 scope.disabled = false;
3177 if ( attrs.disabled ) {
3178 scope.$parent.$watch($parse(attrs.disabled), function(value) {
3179 scope.disabled = !! value;
3180 });
3181 }
3182
3183 scope.select = function() {
3184 if ( !scope.disabled ) {
3185 scope.active = true;
3186 }
3187 };
3188
3189 tabsetCtrl.addTab(scope);
3190 scope.$on('$destroy', function() {
3191 tabsetCtrl.removeTab(scope);
3192 });
3193
3194 //We need to transclude later, once the content container is ready.
3195 //when this link happens, we're inside a tab heading.
3196 scope.$transcludeFn = transclude;
3197 };
3198 }
3199 };
3200}])
3201
3202.directive('tabHeadingTransclude', [function() {
3203 return {
3204 restrict: 'A',
3205 require: '^tab',
3206 link: function(scope, elm, attrs, tabCtrl) {
3207 scope.$watch('headingElement', function updateHeadingElement(heading) {
3208 if (heading) {
3209 elm.html('');
3210 elm.append(heading);
3211 }
3212 });
3213 }
3214 };
3215}])
3216
3217.directive('tabContentTransclude', function() {
3218 return {
3219 restrict: 'A',
3220 require: '^tabset',
3221 link: function(scope, elm, attrs) {
3222 var tab = scope.$eval(attrs.tabContentTransclude);
3223
3224 //Now our tab is ready to be transcluded: both the tab heading area
3225 //and the tab content area are loaded. Transclude 'em both.
3226 tab.$transcludeFn(tab.$parent, function(contents) {
3227 angular.forEach(contents, function(node) {
3228 if (isTabHeading(node)) {
3229 //Let tabHeadingTransclude know.
3230 tab.headingElement = node;
3231 } else {
3232 elm.append(node);
3233 }
3234 });
3235 });
3236 }
3237 };
3238 function isTabHeading(node) {
3239 return node.tagName && (
3240 node.hasAttribute('tab-heading') ||
3241 node.hasAttribute('data-tab-heading') ||
3242 node.tagName.toLowerCase() === 'tab-heading' ||
3243 node.tagName.toLowerCase() === 'data-tab-heading'
3244 );
3245 }
3246})
3247
3248;
3249
3250angular.module('ui.bootstrap.timepicker', [])
3251
3252.constant('timepickerConfig', {
3253 hourStep: 1,
3254 minuteStep: 1,
3255 showMeridian: true,
3256 meridians: null,
3257 readonlyInput: false,
3258 mousewheel: true
3259})
3260
3261.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
3262 var selected = new Date(),
3263 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
3264 meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
3265
3266 this.init = function( ngModelCtrl_, inputs ) {
3267 ngModelCtrl = ngModelCtrl_;
3268 ngModelCtrl.$render = this.render;
3269
3270 var hoursInputEl = inputs.eq(0),
3271 minutesInputEl = inputs.eq(1);
3272
3273 var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
3274 if ( mousewheel ) {
3275 this.setupMousewheelEvents( hoursInputEl, minutesInputEl );
3276 }
3277
3278 $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
3279 this.setupInputEvents( hoursInputEl, minutesInputEl );
3280 };
3281
3282 var hourStep = timepickerConfig.hourStep;
3283 if ($attrs.hourStep) {
3284 $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
3285 hourStep = parseInt(value, 10);
3286 });
3287 }
3288
3289 var minuteStep = timepickerConfig.minuteStep;
3290 if ($attrs.minuteStep) {
3291 $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
3292 minuteStep = parseInt(value, 10);
3293 });
3294 }
3295
3296 // 12H / 24H mode
3297 $scope.showMeridian = timepickerConfig.showMeridian;
3298 if ($attrs.showMeridian) {
3299 $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
3300 $scope.showMeridian = !!value;
3301
3302 if ( ngModelCtrl.$error.time ) {
3303 // Evaluate from template
3304 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
3305 if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
3306 selected.setHours( hours );
3307 refresh();
3308 }
3309 } else {
3310 updateTemplate();
3311 }
3312 });
3313 }
3314
3315 // Get $scope.hours in 24H mode if valid
3316 function getHoursFromTemplate ( ) {
3317 var hours = parseInt( $scope.hours, 10 );
3318 var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
3319 if ( !valid ) {
3320 return undefined;
3321 }
3322
3323 if ( $scope.showMeridian ) {
3324 if ( hours === 12 ) {
3325 hours = 0;
3326 }
3327 if ( $scope.meridian === meridians[1] ) {
3328 hours = hours + 12;
3329 }
3330 }
3331 return hours;
3332 }
3333
3334 function getMinutesFromTemplate() {
3335 var minutes = parseInt($scope.minutes, 10);
3336 return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
3337 }
3338
3339 function pad( value ) {
3340 return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
3341 }
3342
3343 // Respond on mousewheel spin
3344 this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) {
3345 var isScrollingUp = function(e) {
3346 if (e.originalEvent) {
3347 e = e.originalEvent;
3348 }
3349 //pick correct delta variable depending on event
3350 var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
3351 return (e.detail || delta > 0);
3352 };
3353
3354 hoursInputEl.bind('mousewheel wheel', function(e) {
3355 $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() );
3356 e.preventDefault();
3357 });
3358
3359 minutesInputEl.bind('mousewheel wheel', function(e) {