middleman-angularjs 0.0.1 → 0.0.2

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