angular-pack 0.0.1

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