angular-strap-rails 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +17 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +45 -0
  7. data/Rakefile +1 -0
  8. data/angular-strap-rails.gemspec +15 -0
  9. data/lib/angular-strap-rails.rb +8 -0
  10. data/lib/angular-strap-rails/version.rb +5 -0
  11. data/vendor/.DS_Store +0 -0
  12. data/vendor/assets/.DS_Store +0 -0
  13. data/vendor/assets/javascripts/.DS_Store +0 -0
  14. data/vendor/assets/javascripts/angular-strap.coffee +0 -0
  15. data/vendor/assets/javascripts/angular-strap/.DS_Store +0 -0
  16. data/vendor/assets/javascripts/angular-strap/datepicker.coffee +11 -0
  17. data/vendor/assets/javascripts/angular-strap/modal.coffee +9 -0
  18. data/vendor/assets/javascripts/dist/angular-strap.js +3682 -0
  19. data/vendor/assets/javascripts/dist/angular-strap.tpl.js +100 -0
  20. data/vendor/assets/javascripts/dist/modules/affix.js +191 -0
  21. data/vendor/assets/javascripts/dist/modules/alert.js +114 -0
  22. data/vendor/assets/javascripts/dist/modules/alert.tpl.js +14 -0
  23. data/vendor/assets/javascripts/dist/modules/aside.js +96 -0
  24. data/vendor/assets/javascripts/dist/modules/aside.tpl.js +14 -0
  25. data/vendor/assets/javascripts/dist/modules/button.js +141 -0
  26. data/vendor/assets/javascripts/dist/modules/date-parser.js +150 -0
  27. data/vendor/assets/javascripts/dist/modules/datepicker.js +583 -0
  28. data/vendor/assets/javascripts/dist/modules/datepicker.tpl.js +14 -0
  29. data/vendor/assets/javascripts/dist/modules/debounce.js +60 -0
  30. data/vendor/assets/javascripts/dist/modules/dimensions.js +142 -0
  31. data/vendor/assets/javascripts/dist/modules/dropdown.js +124 -0
  32. data/vendor/assets/javascripts/dist/modules/dropdown.tpl.js +14 -0
  33. data/vendor/assets/javascripts/dist/modules/modal.js +282 -0
  34. data/vendor/assets/javascripts/dist/modules/modal.tpl.js +14 -0
  35. data/vendor/assets/javascripts/dist/modules/navbar.js +55 -0
  36. data/vendor/assets/javascripts/dist/modules/parse-options.js +51 -0
  37. data/vendor/assets/javascripts/dist/modules/popover.js +100 -0
  38. data/vendor/assets/javascripts/dist/modules/popover.tpl.js +14 -0
  39. data/vendor/assets/javascripts/dist/modules/raf.js +45 -0
  40. data/vendor/assets/javascripts/dist/modules/scrollspy.js +229 -0
  41. data/vendor/assets/javascripts/dist/modules/select.js +281 -0
  42. data/vendor/assets/javascripts/dist/modules/select.tpl.js +14 -0
  43. data/vendor/assets/javascripts/dist/modules/tab.js +69 -0
  44. data/vendor/assets/javascripts/dist/modules/tab.tpl.js +14 -0
  45. data/vendor/assets/javascripts/dist/modules/timepicker.js +430 -0
  46. data/vendor/assets/javascripts/dist/modules/timepicker.tpl.js +14 -0
  47. data/vendor/assets/javascripts/dist/modules/tooltip.js +405 -0
  48. data/vendor/assets/javascripts/dist/modules/tooltip.tpl.js +14 -0
  49. data/vendor/assets/javascripts/dist/modules/typeahead.js +225 -0
  50. data/vendor/assets/javascripts/dist/modules/typeahead.tpl.js +14 -0
  51. data/vendor/assets/stylesheets/angular-strap.css +564 -0
  52. metadata +94 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * angular-strap
3
+ * @version v2.0.1 - 2014-04-10
4
+ * @link http://mgcrea.github.io/angular-strap
5
+ * @author Olivier Louvignes (olivier@mg-crea.com)
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ 'use strict';
9
+ angular.module('mgcrea.ngStrap.select', [
10
+ 'mgcrea.ngStrap.tooltip',
11
+ 'mgcrea.ngStrap.helpers.parseOptions'
12
+ ]).provider('$select', function () {
13
+ var defaults = this.defaults = {
14
+ animation: 'am-fade',
15
+ prefixClass: 'select',
16
+ placement: 'bottom-left',
17
+ template: 'select/select.tpl.html',
18
+ trigger: 'focus',
19
+ container: false,
20
+ keyboard: true,
21
+ html: false,
22
+ delay: 0,
23
+ multiple: false,
24
+ sort: true,
25
+ caretHtml: '&nbsp;<span class="caret"></span>',
26
+ placeholder: 'Choose among the following...',
27
+ maxLength: 3,
28
+ maxLengthHtml: 'selected'
29
+ };
30
+ this.$get = [
31
+ '$window',
32
+ '$document',
33
+ '$rootScope',
34
+ '$tooltip',
35
+ function ($window, $document, $rootScope, $tooltip) {
36
+ var bodyEl = angular.element($window.document.body);
37
+ var isTouch = 'createTouch' in $window.document;
38
+ function SelectFactory(element, controller, config) {
39
+ var $select = {};
40
+ // Common vars
41
+ var options = angular.extend({}, defaults, config);
42
+ $select = $tooltip(element, options);
43
+ var parentScope = config.scope;
44
+ var scope = $select.$scope;
45
+ scope.$matches = [];
46
+ scope.$activeIndex = 0;
47
+ scope.$isMultiple = options.multiple;
48
+ scope.$activate = function (index) {
49
+ scope.$$postDigest(function () {
50
+ $select.activate(index);
51
+ });
52
+ };
53
+ scope.$select = function (index, evt) {
54
+ scope.$$postDigest(function () {
55
+ $select.select(index);
56
+ });
57
+ };
58
+ scope.$isVisible = function () {
59
+ return $select.$isVisible();
60
+ };
61
+ scope.$isActive = function (index) {
62
+ return $select.$isActive(index);
63
+ };
64
+ // Public methods
65
+ $select.update = function (matches) {
66
+ scope.$matches = matches;
67
+ $select.$updateActiveIndex();
68
+ };
69
+ $select.activate = function (index) {
70
+ if (options.multiple) {
71
+ scope.$activeIndex.sort();
72
+ $select.$isActive(index) ? scope.$activeIndex.splice(scope.$activeIndex.indexOf(index), 1) : scope.$activeIndex.push(index);
73
+ if (options.sort)
74
+ scope.$activeIndex.sort();
75
+ } else {
76
+ scope.$activeIndex = index;
77
+ }
78
+ return scope.$activeIndex;
79
+ };
80
+ $select.select = function (index) {
81
+ var value = scope.$matches[index].value;
82
+ $select.activate(index);
83
+ if (options.multiple) {
84
+ controller.$setViewValue(scope.$activeIndex.map(function (index) {
85
+ return scope.$matches[index].value;
86
+ }));
87
+ } else {
88
+ controller.$setViewValue(value);
89
+ }
90
+ controller.$render();
91
+ if (parentScope)
92
+ parentScope.$digest();
93
+ // Hide if single select
94
+ if (!options.multiple) {
95
+ $select.hide();
96
+ }
97
+ // Emit event
98
+ scope.$emit('$select.select', value, index);
99
+ };
100
+ // Protected methods
101
+ $select.$updateActiveIndex = function () {
102
+ if (controller.$modelValue && scope.$matches.length) {
103
+ if (options.multiple && angular.isArray(controller.$modelValue)) {
104
+ scope.$activeIndex = controller.$modelValue.map(function (value) {
105
+ return $select.$getIndex(value);
106
+ });
107
+ } else {
108
+ scope.$activeIndex = $select.$getIndex(controller.$modelValue);
109
+ }
110
+ } else if (scope.$activeIndex >= scope.$matches.length) {
111
+ scope.$activeIndex = options.multiple ? [] : 0;
112
+ }
113
+ };
114
+ $select.$isVisible = function () {
115
+ if (!options.minLength || !controller) {
116
+ return scope.$matches.length;
117
+ }
118
+ // minLength support
119
+ return scope.$matches.length && controller.$viewValue.length >= options.minLength;
120
+ };
121
+ $select.$isActive = function (index) {
122
+ if (options.multiple) {
123
+ return scope.$activeIndex.indexOf(index) !== -1;
124
+ } else {
125
+ return scope.$activeIndex === index;
126
+ }
127
+ };
128
+ $select.$getIndex = function (value) {
129
+ var l = scope.$matches.length, i = l;
130
+ if (!l)
131
+ return;
132
+ for (i = l; i--;) {
133
+ if (scope.$matches[i].value === value)
134
+ break;
135
+ }
136
+ if (i < 0)
137
+ return;
138
+ return i;
139
+ };
140
+ $select.$onMouseDown = function (evt) {
141
+ // Prevent blur on mousedown on .dropdown-menu
142
+ evt.preventDefault();
143
+ evt.stopPropagation();
144
+ // Emulate click for mobile devices
145
+ if (isTouch) {
146
+ var targetEl = angular.element(evt.target);
147
+ targetEl.triggerHandler('click');
148
+ }
149
+ };
150
+ $select.$onKeyDown = function (evt) {
151
+ if (!/(9|13|38|40)/.test(evt.keyCode))
152
+ return;
153
+ evt.preventDefault();
154
+ evt.stopPropagation();
155
+ // Select with enter
156
+ if (evt.keyCode === 13 || evt.keyCode === 9) {
157
+ return $select.select(scope.$activeIndex);
158
+ }
159
+ // Navigate with keyboard
160
+ if (evt.keyCode === 38 && scope.$activeIndex > 0)
161
+ scope.$activeIndex--;
162
+ else if (evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1)
163
+ scope.$activeIndex++;
164
+ else if (angular.isUndefined(scope.$activeIndex))
165
+ scope.$activeIndex = 0;
166
+ scope.$digest();
167
+ };
168
+ // Overrides
169
+ var _show = $select.show;
170
+ $select.show = function () {
171
+ _show();
172
+ if (options.multiple) {
173
+ $select.$element.addClass('select-multiple');
174
+ }
175
+ setTimeout(function () {
176
+ $select.$element.on(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown);
177
+ if (options.keyboard) {
178
+ element.on('keydown', $select.$onKeyDown);
179
+ }
180
+ });
181
+ };
182
+ var _hide = $select.hide;
183
+ $select.hide = function () {
184
+ $select.$element.off(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown);
185
+ if (options.keyboard) {
186
+ element.off('keydown', $select.$onKeyDown);
187
+ }
188
+ _hide();
189
+ };
190
+ return $select;
191
+ }
192
+ SelectFactory.defaults = defaults;
193
+ return SelectFactory;
194
+ }
195
+ ];
196
+ }).directive('bsSelect', [
197
+ '$window',
198
+ '$parse',
199
+ '$q',
200
+ '$select',
201
+ '$parseOptions',
202
+ function ($window, $parse, $q, $select, $parseOptions) {
203
+ var defaults = $select.defaults;
204
+ return {
205
+ restrict: 'EAC',
206
+ require: 'ngModel',
207
+ link: function postLink(scope, element, attr, controller) {
208
+ // Directive options
209
+ var options = { scope: scope };
210
+ angular.forEach([
211
+ 'placement',
212
+ 'container',
213
+ 'delay',
214
+ 'trigger',
215
+ 'keyboard',
216
+ 'html',
217
+ 'animation',
218
+ 'template',
219
+ 'placeholder',
220
+ 'multiple',
221
+ 'maxLength',
222
+ 'maxLengthHtml'
223
+ ], function (key) {
224
+ if (angular.isDefined(attr[key]))
225
+ options[key] = attr[key];
226
+ });
227
+ // Add support for select markup
228
+ if (element[0].nodeName.toLowerCase() === 'select') {
229
+ var inputEl = element;
230
+ inputEl.css('display', 'none');
231
+ element = angular.element('<button type="button" class="btn btn-default"></button>');
232
+ inputEl.after(element);
233
+ }
234
+ // Build proper ngOptions
235
+ var parsedOptions = $parseOptions(attr.ngOptions);
236
+ // Initialize select
237
+ var select = $select(element, controller, options);
238
+ // Watch ngOptions values before filtering for changes
239
+ var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').trim();
240
+ scope.$watch(watchedOptions, function (newValue, oldValue) {
241
+ // console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue);
242
+ parsedOptions.valuesFn(scope, controller).then(function (values) {
243
+ select.update(values);
244
+ controller.$render();
245
+ });
246
+ }, true);
247
+ // Watch model for changes
248
+ scope.$watch(attr.ngModel, function (newValue, oldValue) {
249
+ // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue);
250
+ select.$updateActiveIndex();
251
+ }, true);
252
+ // Model rendering in view
253
+ controller.$render = function () {
254
+ // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue);
255
+ var selected, index;
256
+ if (options.multiple && angular.isArray(controller.$modelValue)) {
257
+ selected = controller.$modelValue.map(function (value) {
258
+ index = select.$getIndex(value);
259
+ return angular.isDefined(index) ? select.$scope.$matches[index].label : false;
260
+ }).filter(angular.isDefined);
261
+ if (selected.length > (options.maxLength || defaults.maxLength)) {
262
+ selected = selected.length + ' ' + (options.maxLengthHtml || defaults.maxLengthHtml);
263
+ } else {
264
+ selected = selected.join(', ');
265
+ }
266
+ } else {
267
+ index = select.$getIndex(controller.$modelValue);
268
+ selected = angular.isDefined(index) ? select.$scope.$matches[index].label : false;
269
+ }
270
+ element.html((selected ? selected : attr.placeholder || defaults.placeholder) + defaults.caretHtml);
271
+ };
272
+ // Garbage collection
273
+ scope.$on('$destroy', function () {
274
+ select.destroy();
275
+ options = null;
276
+ select = null;
277
+ });
278
+ }
279
+ };
280
+ }
281
+ ]);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * angular-strap
3
+ * @version v2.0.1 - 2014-04-10
4
+ * @link http://mgcrea.github.io/angular-strap
5
+ * @author Olivier Louvignes (olivier@mg-crea.com)
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ 'use strict';
9
+ angular.module('mgcrea.ngStrap.select').run([
10
+ '$templateCache',
11
+ function ($templateCache) {
12
+ $templateCache.put('select/select.tpl.html', '<ul tabindex="-1" class="select dropdown-menu" ng-show="$isVisible()" role="select"><li role="presentation" ng-repeat="match in $matches" ng-class="{active: $isActive($index)}"><a style="cursor: default" role="menuitem" tabindex="-1" ng-click="$select($index, $event)"><span ng-bind="match.label"></span> <i class="glyphicon glyphicon-ok pull-right" ng-if="$isMultiple && $isActive($index)"></i></a></li></ul>');
13
+ }
14
+ ]);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * angular-strap
3
+ * @version v2.0.1 - 2014-04-10
4
+ * @link http://mgcrea.github.io/angular-strap
5
+ * @author Olivier Louvignes (olivier@mg-crea.com)
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ 'use strict';
9
+ angular.module('mgcrea.ngStrap.tab', []).run([
10
+ '$templateCache',
11
+ function ($templateCache) {
12
+ $templateCache.put('$pane', '{{pane.content}}');
13
+ }
14
+ ]).provider('$tab', function () {
15
+ var defaults = this.defaults = {
16
+ animation: 'am-fade',
17
+ template: 'tab/tab.tpl.html'
18
+ };
19
+ this.$get = function () {
20
+ return { defaults: defaults };
21
+ };
22
+ }).directive('bsTabs', [
23
+ '$window',
24
+ '$animate',
25
+ '$tab',
26
+ function ($window, $animate, $tab) {
27
+ var defaults = $tab.defaults;
28
+ return {
29
+ restrict: 'EAC',
30
+ scope: true,
31
+ require: '?ngModel',
32
+ templateUrl: function (element, attr) {
33
+ return attr.template || defaults.template;
34
+ },
35
+ link: function postLink(scope, element, attr, controller) {
36
+ // Directive options
37
+ var options = defaults;
38
+ angular.forEach(['animation'], function (key) {
39
+ if (angular.isDefined(attr[key]))
40
+ options[key] = attr[key];
41
+ });
42
+ // Require scope as an object
43
+ attr.bsTabs && scope.$watch(attr.bsTabs, function (newValue, oldValue) {
44
+ scope.panes = newValue;
45
+ }, true);
46
+ // Add base class
47
+ element.addClass('tabs');
48
+ // Support animations
49
+ if (options.animation) {
50
+ element.addClass(options.animation);
51
+ }
52
+ scope.active = scope.activePane = 0;
53
+ // view -> model
54
+ scope.setActive = function (index, ev) {
55
+ scope.active = index;
56
+ if (controller) {
57
+ controller.$setViewValue(index);
58
+ }
59
+ };
60
+ // model -> view
61
+ if (controller) {
62
+ controller.$render = function () {
63
+ scope.active = controller.$modelValue * 1;
64
+ };
65
+ }
66
+ }
67
+ };
68
+ }
69
+ ]);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * angular-strap
3
+ * @version v2.0.1 - 2014-04-10
4
+ * @link http://mgcrea.github.io/angular-strap
5
+ * @author Olivier Louvignes (olivier@mg-crea.com)
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ 'use strict';
9
+ angular.module('mgcrea.ngStrap.tab').run([
10
+ '$templateCache',
11
+ function ($templateCache) {
12
+ $templateCache.put('tab/tab.tpl.html', '<ul class="nav nav-tabs"><li ng-repeat="pane in panes" ng-class="{active: $index == active}"><a data-toggle="tab" ng-click="setActive($index, $event)" data-index="{{$index}}">{{pane.title}}</a></li></ul><div class="tab-content"><div ng-repeat="pane in panes" class="tab-pane" ng-class="[$index == active ? \'active\' : \'\']" ng-include="pane.template || \'$pane\'"></div></div>');
13
+ }
14
+ ]);
@@ -0,0 +1,430 @@
1
+ /**
2
+ * angular-strap
3
+ * @version v2.0.1 - 2014-04-10
4
+ * @link http://mgcrea.github.io/angular-strap
5
+ * @author Olivier Louvignes (olivier@mg-crea.com)
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ 'use strict';
9
+ angular.module('mgcrea.ngStrap.timepicker', [
10
+ 'mgcrea.ngStrap.helpers.dateParser',
11
+ 'mgcrea.ngStrap.tooltip'
12
+ ]).provider('$timepicker', function () {
13
+ var defaults = this.defaults = {
14
+ animation: 'am-fade',
15
+ prefixClass: 'timepicker',
16
+ placement: 'bottom-left',
17
+ template: 'timepicker/timepicker.tpl.html',
18
+ trigger: 'focus',
19
+ container: false,
20
+ keyboard: true,
21
+ html: false,
22
+ delay: 0,
23
+ useNative: true,
24
+ timeType: 'date',
25
+ timeFormat: 'shortTime',
26
+ autoclose: false,
27
+ minTime: -Infinity,
28
+ maxTime: +Infinity,
29
+ length: 5,
30
+ hourStep: 1,
31
+ minuteStep: 5
32
+ };
33
+ this.$get = [
34
+ '$window',
35
+ '$document',
36
+ '$rootScope',
37
+ '$sce',
38
+ '$locale',
39
+ 'dateFilter',
40
+ '$tooltip',
41
+ function ($window, $document, $rootScope, $sce, $locale, dateFilter, $tooltip) {
42
+ var bodyEl = angular.element($window.document.body);
43
+ var isTouch = 'createTouch' in $window.document;
44
+ var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent);
45
+ if (!defaults.lang)
46
+ defaults.lang = $locale.id;
47
+ function timepickerFactory(element, controller, config) {
48
+ var $timepicker = $tooltip(element, angular.extend({}, defaults, config));
49
+ var parentScope = config.scope;
50
+ var options = $timepicker.$options;
51
+ var scope = $timepicker.$scope;
52
+ // View vars
53
+ var selectedIndex = 0;
54
+ var startDate = controller.$dateValue || new Date();
55
+ var viewDate = {
56
+ hour: startDate.getHours(),
57
+ meridian: startDate.getHours() < 12,
58
+ minute: startDate.getMinutes(),
59
+ second: startDate.getSeconds(),
60
+ millisecond: startDate.getMilliseconds()
61
+ };
62
+ var format = $locale.DATETIME_FORMATS[options.timeFormat] || options.timeFormat;
63
+ var formats = /(h+)[:]?(m+)[ ]?(a?)/i.exec(format).slice(1);
64
+ // Scope methods
65
+ scope.$select = function (date, index) {
66
+ $timepicker.select(date, index);
67
+ };
68
+ scope.$moveIndex = function (value, index) {
69
+ $timepicker.$moveIndex(value, index);
70
+ };
71
+ scope.$switchMeridian = function (date) {
72
+ $timepicker.switchMeridian(date);
73
+ };
74
+ // Public methods
75
+ $timepicker.update = function (date) {
76
+ // console.warn('$timepicker.update() newValue=%o', date);
77
+ if (angular.isDate(date) && !isNaN(date.getTime())) {
78
+ $timepicker.$date = date;
79
+ angular.extend(viewDate, {
80
+ hour: date.getHours(),
81
+ minute: date.getMinutes(),
82
+ second: date.getSeconds(),
83
+ millisecond: date.getMilliseconds()
84
+ });
85
+ $timepicker.$build();
86
+ } else if (!$timepicker.$isBuilt) {
87
+ $timepicker.$build();
88
+ }
89
+ };
90
+ $timepicker.select = function (date, index, keep) {
91
+ // console.warn('$timepicker.select', date, scope.$mode);
92
+ if (!controller.$dateValue || isNaN(controller.$dateValue.getTime()))
93
+ controller.$dateValue = new Date(1970, 0, 1);
94
+ if (!angular.isDate(date))
95
+ date = new Date(date);
96
+ if (index === 0)
97
+ controller.$dateValue.setHours(date.getHours());
98
+ else if (index === 1)
99
+ controller.$dateValue.setMinutes(date.getMinutes());
100
+ controller.$setViewValue(controller.$dateValue);
101
+ controller.$render();
102
+ if (options.autoclose && !keep) {
103
+ $timepicker.hide(true);
104
+ }
105
+ };
106
+ $timepicker.switchMeridian = function (date) {
107
+ var hours = (date || controller.$dateValue).getHours();
108
+ controller.$dateValue.setHours(hours < 12 ? hours + 12 : hours - 12);
109
+ controller.$render();
110
+ };
111
+ // Protected methods
112
+ $timepicker.$build = function () {
113
+ // console.warn('$timepicker.$build() viewDate=%o', viewDate);
114
+ var i, midIndex = scope.midIndex = parseInt(options.length / 2, 10);
115
+ var hours = [], hour;
116
+ for (i = 0; i < options.length; i++) {
117
+ hour = new Date(1970, 0, 1, viewDate.hour - (midIndex - i) * options.hourStep);
118
+ hours.push({
119
+ date: hour,
120
+ label: dateFilter(hour, formats[0]),
121
+ selected: $timepicker.$date && $timepicker.$isSelected(hour, 0),
122
+ disabled: $timepicker.$isDisabled(hour, 0)
123
+ });
124
+ }
125
+ var minutes = [], minute;
126
+ for (i = 0; i < options.length; i++) {
127
+ minute = new Date(1970, 0, 1, 0, viewDate.minute - (midIndex - i) * options.minuteStep);
128
+ minutes.push({
129
+ date: minute,
130
+ label: dateFilter(minute, formats[1]),
131
+ selected: $timepicker.$date && $timepicker.$isSelected(minute, 1),
132
+ disabled: $timepicker.$isDisabled(minute, 1)
133
+ });
134
+ }
135
+ var rows = [];
136
+ for (i = 0; i < options.length; i++) {
137
+ rows.push([
138
+ hours[i],
139
+ minutes[i]
140
+ ]);
141
+ }
142
+ scope.rows = rows;
143
+ scope.showAM = !!formats[2];
144
+ scope.isAM = ($timepicker.$date || hours[midIndex].date).getHours() < 12;
145
+ $timepicker.$isBuilt = true;
146
+ };
147
+ $timepicker.$isSelected = function (date, index) {
148
+ if (!$timepicker.$date)
149
+ return false;
150
+ else if (index === 0) {
151
+ return date.getHours() === $timepicker.$date.getHours();
152
+ } else if (index === 1) {
153
+ return date.getMinutes() === $timepicker.$date.getMinutes();
154
+ }
155
+ };
156
+ $timepicker.$isDisabled = function (date, index) {
157
+ var selectedTime;
158
+ if (index === 0) {
159
+ selectedTime = date.getTime() + viewDate.minute * 60000;
160
+ } else if (index === 1) {
161
+ selectedTime = date.getTime() + viewDate.hour * 3600000;
162
+ }
163
+ return selectedTime < options.minTime || selectedTime > options.maxTime;
164
+ };
165
+ $timepicker.$moveIndex = function (value, index) {
166
+ var targetDate;
167
+ if (index === 0) {
168
+ targetDate = new Date(1970, 0, 1, viewDate.hour + value * options.length, viewDate.minute);
169
+ angular.extend(viewDate, { hour: targetDate.getHours() });
170
+ } else if (index === 1) {
171
+ targetDate = new Date(1970, 0, 1, viewDate.hour, viewDate.minute + value * options.length * options.minuteStep);
172
+ angular.extend(viewDate, { minute: targetDate.getMinutes() });
173
+ }
174
+ $timepicker.$build();
175
+ };
176
+ $timepicker.$onMouseDown = function (evt) {
177
+ // Prevent blur on mousedown on .dropdown-menu
178
+ if (evt.target.nodeName.toLowerCase() !== 'input')
179
+ evt.preventDefault();
180
+ evt.stopPropagation();
181
+ // Emulate click for mobile devices
182
+ if (isTouch) {
183
+ var targetEl = angular.element(evt.target);
184
+ if (targetEl[0].nodeName.toLowerCase() !== 'button') {
185
+ targetEl = targetEl.parent();
186
+ }
187
+ targetEl.triggerHandler('click');
188
+ }
189
+ };
190
+ $timepicker.$onKeyDown = function (evt) {
191
+ if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey)
192
+ return;
193
+ evt.preventDefault();
194
+ evt.stopPropagation();
195
+ // Close on enter
196
+ if (evt.keyCode === 13)
197
+ return $timepicker.hide(true);
198
+ // Navigate with keyboard
199
+ var newDate = new Date($timepicker.$date);
200
+ var hours = newDate.getHours(), hoursLength = dateFilter(newDate, 'h').length;
201
+ var minutes = newDate.getMinutes(), minutesLength = dateFilter(newDate, 'mm').length;
202
+ var lateralMove = /(37|39)/.test(evt.keyCode);
203
+ var count = 2 + !!formats[2] * 1;
204
+ // Navigate indexes (left, right)
205
+ if (lateralMove) {
206
+ if (evt.keyCode === 37)
207
+ selectedIndex = selectedIndex < 1 ? count - 1 : selectedIndex - 1;
208
+ else if (evt.keyCode === 39)
209
+ selectedIndex = selectedIndex < count - 1 ? selectedIndex + 1 : 0;
210
+ }
211
+ // Update values (up, down)
212
+ if (selectedIndex === 0) {
213
+ if (lateralMove)
214
+ return createSelection(0, hoursLength);
215
+ if (evt.keyCode === 38)
216
+ newDate.setHours(hours - parseInt(options.hourStep, 10));
217
+ else if (evt.keyCode === 40)
218
+ newDate.setHours(hours + parseInt(options.hourStep, 10));
219
+ } else if (selectedIndex === 1) {
220
+ if (lateralMove)
221
+ return createSelection(hoursLength + 1, hoursLength + 1 + minutesLength);
222
+ if (evt.keyCode === 38)
223
+ newDate.setMinutes(minutes - parseInt(options.minuteStep, 10));
224
+ else if (evt.keyCode === 40)
225
+ newDate.setMinutes(minutes + parseInt(options.minuteStep, 10));
226
+ } else if (selectedIndex === 2) {
227
+ if (lateralMove)
228
+ return createSelection(hoursLength + 1 + minutesLength + 1, hoursLength + 1 + minutesLength + 3);
229
+ $timepicker.switchMeridian();
230
+ }
231
+ $timepicker.select(newDate, selectedIndex, true);
232
+ parentScope.$digest();
233
+ };
234
+ // Private
235
+ function createSelection(start, end) {
236
+ if (element[0].createTextRange) {
237
+ var selRange = element[0].createTextRange();
238
+ selRange.collapse(true);
239
+ selRange.moveStart('character', start);
240
+ selRange.moveEnd('character', end);
241
+ selRange.select();
242
+ } else if (element[0].setSelectionRange) {
243
+ element[0].setSelectionRange(start, end);
244
+ } else if (angular.isUndefined(element[0].selectionStart)) {
245
+ element[0].selectionStart = start;
246
+ element[0].selectionEnd = end;
247
+ }
248
+ }
249
+ function focusElement() {
250
+ element[0].focus();
251
+ }
252
+ // Overrides
253
+ var _init = $timepicker.init;
254
+ $timepicker.init = function () {
255
+ if (isNative && options.useNative) {
256
+ element.prop('type', 'time');
257
+ element.css('-webkit-appearance', 'textfield');
258
+ return;
259
+ } else if (isTouch) {
260
+ element.prop('type', 'text');
261
+ element.attr('readonly', 'true');
262
+ element.on('click', focusElement);
263
+ }
264
+ _init();
265
+ };
266
+ var _destroy = $timepicker.destroy;
267
+ $timepicker.destroy = function () {
268
+ if (isNative && options.useNative) {
269
+ element.off('click', focusElement);
270
+ }
271
+ _destroy();
272
+ };
273
+ var _show = $timepicker.show;
274
+ $timepicker.show = function () {
275
+ _show();
276
+ setTimeout(function () {
277
+ $timepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown);
278
+ if (options.keyboard) {
279
+ element.on('keydown', $timepicker.$onKeyDown);
280
+ }
281
+ });
282
+ };
283
+ var _hide = $timepicker.hide;
284
+ $timepicker.hide = function (blur) {
285
+ $timepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown);
286
+ if (options.keyboard) {
287
+ element.off('keydown', $timepicker.$onKeyDown);
288
+ }
289
+ _hide(blur);
290
+ };
291
+ return $timepicker;
292
+ }
293
+ timepickerFactory.defaults = defaults;
294
+ return timepickerFactory;
295
+ }
296
+ ];
297
+ }).directive('bsTimepicker', [
298
+ '$window',
299
+ '$parse',
300
+ '$q',
301
+ '$locale',
302
+ 'dateFilter',
303
+ '$timepicker',
304
+ '$dateParser',
305
+ '$timeout',
306
+ function ($window, $parse, $q, $locale, dateFilter, $timepicker, $dateParser, $timeout) {
307
+ var defaults = $timepicker.defaults;
308
+ var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent);
309
+ var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout;
310
+ return {
311
+ restrict: 'EAC',
312
+ require: 'ngModel',
313
+ link: function postLink(scope, element, attr, controller) {
314
+ // Directive options
315
+ var options = {
316
+ scope: scope,
317
+ controller: controller
318
+ };
319
+ angular.forEach([
320
+ 'placement',
321
+ 'container',
322
+ 'delay',
323
+ 'trigger',
324
+ 'keyboard',
325
+ 'html',
326
+ 'animation',
327
+ 'template',
328
+ 'autoclose',
329
+ 'timeType',
330
+ 'timeFormat',
331
+ 'useNative',
332
+ 'hourStep',
333
+ 'minuteStep'
334
+ ], function (key) {
335
+ if (angular.isDefined(attr[key]))
336
+ options[key] = attr[key];
337
+ });
338
+ // Initialize timepicker
339
+ if (isNative && (options.useNative || defaults.useNative))
340
+ options.timeFormat = 'HH:mm';
341
+ var timepicker = $timepicker(element, controller, options);
342
+ options = timepicker.$options;
343
+ // Initialize parser
344
+ var dateParser = $dateParser({
345
+ format: options.timeFormat,
346
+ lang: options.lang
347
+ });
348
+ // Observe attributes for changes
349
+ angular.forEach([
350
+ 'minTime',
351
+ 'maxTime'
352
+ ], function (key) {
353
+ // console.warn('attr.$observe(%s)', key, attr[key]);
354
+ angular.isDefined(attr[key]) && attr.$observe(key, function (newValue) {
355
+ if (newValue === 'now') {
356
+ timepicker.$options[key] = new Date().setFullYear(1970, 0, 1);
357
+ } else if (angular.isString(newValue) && newValue.match(/^".+"$/)) {
358
+ timepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2));
359
+ } else {
360
+ timepicker.$options[key] = dateParser.parse(newValue);
361
+ }
362
+ !isNaN(timepicker.$options[key]) && timepicker.$build();
363
+ });
364
+ });
365
+ // Watch model for changes
366
+ scope.$watch(attr.ngModel, function (newValue, oldValue) {
367
+ // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue, controller.$dateValue);
368
+ timepicker.update(controller.$dateValue);
369
+ }, true);
370
+ // viewValue -> $parsers -> modelValue
371
+ controller.$parsers.unshift(function (viewValue) {
372
+ // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue);
373
+ // Null values should correctly reset the model value & validity
374
+ if (!viewValue) {
375
+ controller.$setValidity('date', true);
376
+ return;
377
+ }
378
+ var parsedTime = dateParser.parse(viewValue, controller.$dateValue);
379
+ if (!parsedTime || isNaN(parsedTime.getTime())) {
380
+ controller.$setValidity('date', false);
381
+ } else {
382
+ var isValid = parsedTime.getTime() >= options.minTime && parsedTime.getTime() <= options.maxTime;
383
+ controller.$setValidity('date', isValid);
384
+ // Only update the model when we have a valid date
385
+ if (isValid)
386
+ controller.$dateValue = parsedTime;
387
+ }
388
+ if (options.timeType === 'string') {
389
+ return dateFilter(viewValue, options.timeFormat);
390
+ } else if (options.timeType === 'number') {
391
+ return controller.$dateValue.getTime();
392
+ } else if (options.timeType === 'iso') {
393
+ return controller.$dateValue.toISOString();
394
+ } else {
395
+ return new Date(controller.$dateValue);
396
+ }
397
+ });
398
+ // modelValue -> $formatters -> viewValue
399
+ controller.$formatters.push(function (modelValue) {
400
+ // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
401
+ var date;
402
+ if (angular.isUndefined(modelValue) || modelValue === null) {
403
+ date = NaN;
404
+ } else if (angular.isDate(modelValue)) {
405
+ date = modelValue;
406
+ } else if (options.timeType === 'string') {
407
+ date = dateParser.parse(modelValue);
408
+ } else {
409
+ date = new Date(modelValue);
410
+ }
411
+ // Setup default value?
412
+ // if(isNaN(date.getTime())) date = new Date(new Date().setMinutes(0) + 36e5);
413
+ controller.$dateValue = date;
414
+ return controller.$dateValue;
415
+ });
416
+ // viewValue -> element
417
+ controller.$render = function () {
418
+ // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue);
419
+ element.val(!controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.timeFormat));
420
+ };
421
+ // Garbage collection
422
+ scope.$on('$destroy', function () {
423
+ timepicker.destroy();
424
+ options = null;
425
+ timepicker = null;
426
+ });
427
+ }
428
+ };
429
+ }
430
+ ]);