dashstrap 0.1.0

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