angular-ui-select-rails 0.9.6 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 445dae31c8cfd6554edfe7d5c27ca16275459ed0
4
- data.tar.gz: 2e629e964410ccdecbabd9354e1c8902cbd0a94c
3
+ metadata.gz: 384b766116a568686a6b6a657535a1e483a23f82
4
+ data.tar.gz: 5d7e87f3cf819ad228c5552656d092f136bcea0f
5
5
  SHA512:
6
- metadata.gz: 9ea5a84ac2a0d67548c018640548f2b5b4498f9606daa89821e6f7dd9bc7ee44c9dd041bb64bfa3d8785a06f445ea7e799383335eee2f599fc27214ea522fc44
7
- data.tar.gz: daca53d17842ade28a74d3b54b23b11f604deac87b5a4d67e928a0d3eda8c556e95881f1c4b2a9b083307e0f97107fe69dbd55b4a94e0d4e90ba450f06a55c3c
6
+ metadata.gz: 9f97468262bffa049c377db920f972a232952cf3d8bea9117431fb6d02aaedbf00cd3db40e5ea19430d649284eff20961f5dc0cc07883e9430629abc58a7e6e7
7
+ data.tar.gz: 31a1ad4ad4061afcc3efe963799e0ca25322117687f94d525d88711abd09553b24b37e20f76f8b410036734b045040eba8595e6c994a63e643f865318588850b
@@ -2,7 +2,7 @@ module Angular
2
2
  module Ui
3
3
  module Select
4
4
  module Rails
5
- VERSION = "0.9.6"
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
8
8
  end
@@ -1,15 +1,15 @@
1
1
  /*!
2
2
  * ui-select
3
3
  * http://github.com/angular-ui/ui-select
4
- * Version: 0.9.5 - 2014-12-12T16:07:20.856Z
4
+ * Version: 0.12.0 - 2015-05-28T07:44:11.360Z
5
5
  * License: MIT
6
6
  */
7
7
 
8
8
 
9
- (function () {
10
- "use strict";
9
+ (function () {
10
+ "use strict";
11
11
 
12
- var KEY = {
12
+ var KEY = {
13
13
  TAB: 9,
14
14
  ENTER: 13,
15
15
  ESC: 27,
@@ -58,847 +58,739 @@
58
58
  }
59
59
  };
60
60
 
61
- /**
62
- * Add querySelectorAll() to jqLite.
63
- *
64
- * jqLite find() is limited to lookups by tag name.
65
- * TODO This will change with future versions of AngularJS, to be removed when this happens
66
- *
67
- * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
68
- * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
69
- */
70
- if (angular.element.prototype.querySelectorAll === undefined) {
71
- angular.element.prototype.querySelectorAll = function(selector) {
72
- return angular.element(this[0].querySelectorAll(selector));
73
- };
74
- }
61
+ /**
62
+ * Add querySelectorAll() to jqLite.
63
+ *
64
+ * jqLite find() is limited to lookups by tag name.
65
+ * TODO This will change with future versions of AngularJS, to be removed when this happens
66
+ *
67
+ * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
68
+ * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
69
+ */
70
+ if (angular.element.prototype.querySelectorAll === undefined) {
71
+ angular.element.prototype.querySelectorAll = function(selector) {
72
+ return angular.element(this[0].querySelectorAll(selector));
73
+ };
74
+ }
75
75
 
76
- /**
77
- * Add closest() to jqLite.
78
- */
79
- if (angular.element.prototype.closest === undefined) {
80
- angular.element.prototype.closest = function( selector) {
81
- var elem = this[0];
82
- var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
83
-
84
- while (elem) {
85
- if (matchesSelector.bind(elem)(selector)) {
86
- return elem;
87
- } else {
88
- elem = elem.parentElement;
89
- }
76
+ /**
77
+ * Add closest() to jqLite.
78
+ */
79
+ if (angular.element.prototype.closest === undefined) {
80
+ angular.element.prototype.closest = function( selector) {
81
+ var elem = this[0];
82
+ var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
83
+
84
+ while (elem) {
85
+ if (matchesSelector.bind(elem)(selector)) {
86
+ return elem;
87
+ } else {
88
+ elem = elem.parentElement;
89
+ }
90
+ }
91
+ return false;
92
+ };
93
+ }
94
+
95
+ var latestId = 0;
96
+
97
+ var uis = angular.module('ui.select', [])
98
+
99
+ .constant('uiSelectConfig', {
100
+ theme: 'bootstrap',
101
+ searchEnabled: true,
102
+ sortable: false,
103
+ placeholder: '', // Empty by default, like HTML tag <select>
104
+ refreshDelay: 1000, // In milliseconds
105
+ closeOnSelect: true,
106
+ generateId: function() {
107
+ return latestId++;
108
+ },
109
+ appendToBody: false
110
+ })
111
+
112
+ // See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
113
+ .service('uiSelectMinErr', function() {
114
+ var minErr = angular.$$minErr('ui.select');
115
+ return function() {
116
+ var error = minErr.apply(this, arguments);
117
+ var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
118
+ return new Error(message);
119
+ };
120
+ })
121
+
122
+ // Recreates old behavior of ng-transclude. Used internally.
123
+ .directive('uisTranscludeAppend', function () {
124
+ return {
125
+ link: function (scope, element, attrs, ctrl, transclude) {
126
+ transclude(scope, function (clone) {
127
+ element.append(clone);
128
+ });
90
129
  }
91
- return false;
92
130
  };
93
- }
131
+ })
94
132
 
95
- angular.module('ui.select', [])
96
-
97
- .constant('uiSelectConfig', {
98
- theme: 'bootstrap',
99
- searchEnabled: true,
100
- placeholder: '', // Empty by default, like HTML tag <select>
101
- refreshDelay: 1000, // In milliseconds
102
- closeOnSelect: true
103
- })
104
-
105
- // See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
106
- .service('uiSelectMinErr', function() {
107
- var minErr = angular.$$minErr('ui.select');
108
- return function() {
109
- var error = minErr.apply(this, arguments);
110
- var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
111
- return new Error(message);
112
- };
113
- })
133
+ /**
134
+ * Highlights text that matches $select.search.
135
+ *
136
+ * Taken from AngularUI Bootstrap Typeahead
137
+ * See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340
138
+ */
139
+ .filter('highlight', function() {
140
+ function escapeRegexp(queryToEscape) {
141
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
142
+ }
114
143
 
115
- /**
116
- * Parses "repeat" attribute.
117
- *
118
- * Taken from AngularJS ngRepeat source code
119
- * See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211
120
- *
121
- * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
122
- * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
123
- */
124
- .service('RepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
125
- var self = this;
126
-
127
- /**
128
- * Example:
129
- * expression = "address in addresses | filter: {street: $select.search} track by $index"
130
- * itemName = "address",
131
- * source = "addresses | filter: {street: $select.search}",
132
- * trackByExp = "$index",
133
- */
134
- self.parse = function(expression) {
135
-
136
- var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
137
-
138
- if (!match) {
139
- throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
140
- expression);
141
- }
144
+ return function(matchItem, query) {
145
+ return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
146
+ };
147
+ })
142
148
 
143
- return {
144
- itemName: match[2], // (lhs) Left-hand side,
145
- source: $parse(match[3]),
146
- trackByExp: match[4],
147
- modelMapper: $parse(match[1] || match[2])
148
- };
149
+ /**
150
+ * A read-only equivalent of jQuery's offset function: http://api.jquery.com/offset/
151
+ *
152
+ * Taken from AngularUI Bootstrap Position:
153
+ * See https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js#L70
154
+ */
155
+ .factory('uisOffset',
156
+ ['$document', '$window',
157
+ function ($document, $window) {
149
158
 
159
+ return function(element) {
160
+ var boundingClientRect = element[0].getBoundingClientRect();
161
+ return {
162
+ width: boundingClientRect.width || element.prop('offsetWidth'),
163
+ height: boundingClientRect.height || element.prop('offsetHeight'),
164
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
165
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
150
166
  };
167
+ };
168
+ }]);
169
+
170
+ uis.directive('uiSelectChoices',
171
+ ['uiSelectConfig', 'uisRepeatParser', 'uiSelectMinErr', '$compile',
172
+ function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile) {
173
+
174
+ return {
175
+ restrict: 'EA',
176
+ require: '^uiSelect',
177
+ replace: true,
178
+ transclude: true,
179
+ templateUrl: function(tElement) {
180
+ // Gets theme attribute from parent (ui-select)
181
+ var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
182
+ return theme + '/choices.tpl.html';
183
+ },
151
184
 
152
- self.getGroupNgRepeatExpression = function() {
153
- return '$group in $select.groups';
154
- };
185
+ compile: function(tElement, tAttrs) {
155
186
 
156
- self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) {
157
- var expression = itemName + ' in ' + (grouped ? '$group.items' : source);
158
- if (trackByExp) {
159
- expression += ' track by ' + trackByExp;
160
- }
161
- return expression;
162
- };
163
- }])
187
+ if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
164
188
 
165
- /**
166
- * Contains ui-select "intelligence".
167
- *
168
- * The goal is to limit dependency on the DOM whenever possible and
169
- * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
170
- */
171
- .controller('uiSelectCtrl',
172
- ['$scope', '$element', '$timeout', '$filter', 'RepeatParser', 'uiSelectMinErr', 'uiSelectConfig',
173
- function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) {
189
+ return function link(scope, element, attrs, $select, transcludeFn) {
174
190
 
175
- var ctrl = this;
191
+ // var repeat = RepeatParser.parse(attrs.repeat);
192
+ var groupByExp = attrs.groupBy;
193
+ var groupFilterExp = attrs.groupFilter;
176
194
 
177
- var EMPTY_SEARCH = '';
195
+ $select.parseRepeatAttr(attrs.repeat, groupByExp, groupFilterExp); //Result ready at $select.parserResult
178
196
 
179
- ctrl.placeholder = undefined;
180
- ctrl.search = EMPTY_SEARCH;
181
- ctrl.activeIndex = 0;
182
- ctrl.activeMatchIndex = -1;
183
- ctrl.items = [];
184
- ctrl.selected = undefined;
185
- ctrl.open = false;
186
- ctrl.focus = false;
187
- ctrl.focusser = undefined; //Reference to input element used to handle focus events
188
- ctrl.disabled = undefined; // Initialized inside uiSelect directive link function
189
- ctrl.searchEnabled = undefined; // Initialized inside uiSelect directive link function
190
- ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function
191
- ctrl.refreshDelay = undefined; // Initialized inside uiSelectChoices directive link function
192
- ctrl.multiple = false; // Initialized inside uiSelect directive link function
193
- ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelect directive link function
194
- ctrl.tagging = {isActivated: false, fct: undefined};
195
- ctrl.taggingTokens = {isActivated: false, tokens: undefined};
196
- ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelect directive link function
197
- ctrl.closeOnSelect = true; // Initialized inside uiSelect directive link function
198
- ctrl.clickTriggeredSelect = false;
199
- ctrl.$filter = $filter;
200
-
201
- ctrl.isEmpty = function() {
202
- return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === '';
203
- };
197
+ $select.disableChoiceExpression = attrs.uiDisableChoice;
198
+ $select.onHighlightCallback = attrs.onHighlight;
204
199
 
205
- var _searchInput = $element.querySelectorAll('input.ui-select-search');
206
- if (_searchInput.length !== 1) {
207
- throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", _searchInput.length);
208
- }
200
+ if(groupByExp) {
201
+ var groups = element.querySelectorAll('.ui-select-choices-group');
202
+ if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length);
203
+ groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
204
+ }
209
205
 
210
- // Most of the time the user does not want to empty the search input when in typeahead mode
211
- function _resetSearchInput() {
212
- if (ctrl.resetSearchInput || (ctrl.resetSearchInput === undefined && uiSelectConfig.resetSearchInput)) {
213
- ctrl.search = EMPTY_SEARCH;
214
- //reset activeIndex
215
- if (ctrl.selected && ctrl.items.length && !ctrl.multiple) {
216
- ctrl.activeIndex = ctrl.items.indexOf(ctrl.selected);
206
+ var choices = element.querySelectorAll('.ui-select-choices-row');
207
+ if (choices.length !== 1) {
208
+ throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
217
209
  }
218
- }
219
- }
220
210
 
221
- // When the user clicks on ui-select, displays the dropdown list
222
- ctrl.activate = function(initSearchValue, avoidReset) {
223
- if (!ctrl.disabled && !ctrl.open) {
224
- if(!avoidReset) _resetSearchInput();
225
- ctrl.focusser.prop('disabled', true); //Will reactivate it on .close()
226
- ctrl.open = true;
227
- ctrl.activeMatchIndex = -1;
211
+ choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp))
212
+ .attr('ng-if', '$select.open') //Prevent unnecessary watches when dropdown is closed
213
+ .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')')
214
+ .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ',false,$event)');
228
215
 
229
- ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
216
+ var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner');
217
+ if (rowsInner.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length);
218
+ rowsInner.attr('uis-transclude-append', ''); //Adding uisTranscludeAppend directive to row element after choices element has ngRepeat
230
219
 
231
- // ensure that the index is set to zero for tagging variants
232
- // that where first option is auto-selected
233
- if ( ctrl.activeIndex === -1 && ctrl.taggingLabel !== false ) {
234
- ctrl.activeIndex = 0;
235
- }
220
+ $compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend
236
221
 
237
- // Give it time to appear before focus
238
- $timeout(function() {
239
- ctrl.search = initSearchValue || ctrl.search;
240
- _searchInput[0].focus();
222
+ scope.$watch('$select.search', function(newValue) {
223
+ if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
224
+ $select.activeIndex = $select.tagging.isActivated ? -1 : 0;
225
+ $select.refresh(attrs.refresh);
241
226
  });
242
- }
243
- };
244
227
 
245
- ctrl.findGroupByName = function(name) {
246
- return ctrl.groups && ctrl.groups.filter(function(group) {
247
- return group.name === name;
248
- })[0];
249
- };
250
-
251
- ctrl.parseRepeatAttr = function(repeatAttr, groupByExp) {
252
- function updateGroups(items) {
253
- ctrl.groups = [];
254
- angular.forEach(items, function(item) {
255
- var groupFn = $scope.$eval(groupByExp);
256
- var groupName = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
257
- var group = ctrl.findGroupByName(groupName);
258
- if(group) {
259
- group.items.push(item);
260
- }
261
- else {
262
- ctrl.groups.push({name: groupName, items: [item]});
263
- }
228
+ attrs.$observe('refreshDelay', function() {
229
+ // $eval() is needed otherwise we get a string instead of a number
230
+ var refreshDelay = scope.$eval(attrs.refreshDelay);
231
+ $select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay;
264
232
  });
265
- ctrl.items = [];
266
- ctrl.groups.forEach(function(group) {
267
- ctrl.items = ctrl.items.concat(group.items);
268
- });
269
- }
233
+ };
234
+ }
235
+ };
236
+ }]);
237
+
238
+ /**
239
+ * Contains ui-select "intelligence".
240
+ *
241
+ * The goal is to limit dependency on the DOM whenever possible and
242
+ * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
243
+ */
244
+ uis.controller('uiSelectCtrl',
245
+ ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig',
246
+ function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) {
247
+
248
+ var ctrl = this;
249
+
250
+ var EMPTY_SEARCH = '';
251
+
252
+ ctrl.placeholder = uiSelectConfig.placeholder;
253
+ ctrl.searchEnabled = uiSelectConfig.searchEnabled;
254
+ ctrl.sortable = uiSelectConfig.sortable;
255
+ ctrl.refreshDelay = uiSelectConfig.refreshDelay;
256
+
257
+ ctrl.removeSelected = false; //If selected item(s) should be removed from dropdown list
258
+ ctrl.closeOnSelect = true; //Initialized inside uiSelect directive link function
259
+ ctrl.search = EMPTY_SEARCH;
260
+
261
+ ctrl.activeIndex = 0; //Dropdown of choices
262
+ ctrl.items = []; //All available choices
263
+
264
+ ctrl.open = false;
265
+ ctrl.focus = false;
266
+ ctrl.disabled = false;
267
+ ctrl.selected = undefined;
268
+
269
+ ctrl.focusser = undefined; //Reference to input element used to handle focus events
270
+ ctrl.resetSearchInput = true;
271
+ ctrl.multiple = undefined; // Initialized inside uiSelect directive link function
272
+ ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelectChoices directive link function
273
+ ctrl.tagging = {isActivated: false, fct: undefined};
274
+ ctrl.taggingTokens = {isActivated: false, tokens: undefined};
275
+ ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelectMatch directive link function
276
+ ctrl.clickTriggeredSelect = false;
277
+ ctrl.$filter = $filter;
278
+
279
+ ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
280
+ if (ctrl.searchInput.length !== 1) {
281
+ throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
282
+ }
283
+
284
+ ctrl.isEmpty = function() {
285
+ return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === '';
286
+ };
270
287
 
271
- function setPlainItems(items) {
272
- ctrl.items = items;
288
+ // Most of the time the user does not want to empty the search input when in typeahead mode
289
+ function _resetSearchInput() {
290
+ if (ctrl.resetSearchInput || (ctrl.resetSearchInput === undefined && uiSelectConfig.resetSearchInput)) {
291
+ ctrl.search = EMPTY_SEARCH;
292
+ //reset activeIndex
293
+ if (ctrl.selected && ctrl.items.length && !ctrl.multiple) {
294
+ ctrl.activeIndex = ctrl.items.indexOf(ctrl.selected);
273
295
  }
296
+ }
297
+ }
274
298
 
275
- var setItemsFn = groupByExp ? updateGroups : setPlainItems;
299
+ function _groupsFilter(groups, groupNames) {
300
+ var i, j, result = [];
301
+ for(i = 0; i < groupNames.length ;i++){
302
+ for(j = 0; j < groups.length ;j++){
303
+ if(groups[j].name == [groupNames[i]]){
304
+ result.push(groups[j]);
305
+ }
306
+ }
307
+ }
308
+ return result;
309
+ }
276
310
 
277
- ctrl.parserResult = RepeatParser.parse(repeatAttr);
311
+ // When the user clicks on ui-select, displays the dropdown list
312
+ ctrl.activate = function(initSearchValue, avoidReset) {
313
+ if (!ctrl.disabled && !ctrl.open) {
314
+ if(!avoidReset) _resetSearchInput();
278
315
 
279
- ctrl.isGrouped = !!groupByExp;
280
- ctrl.itemProperty = ctrl.parserResult.itemName;
316
+ $scope.$broadcast('uis:activate');
281
317
 
282
- // See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
283
- $scope.$watchCollection(ctrl.parserResult.source, function(items) {
318
+ ctrl.open = true;
284
319
 
285
- if (items === undefined || items === null) {
286
- // If the user specifies undefined or null => reset the collection
287
- // Special case: items can be undefined if the user did not initialized the collection on the scope
288
- // i.e $scope.addresses = [] is missing
289
- ctrl.items = [];
290
- } else {
291
- if (!angular.isArray(items)) {
292
- throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
293
- } else {
294
- if (ctrl.multiple){
295
- //Remove already selected items (ex: while searching)
296
- var filteredItems = items.filter(function(i) {return ctrl.selected.indexOf(i) < 0;});
297
- setItemsFn(filteredItems);
298
- }else{
299
- setItemsFn(items);
300
- }
301
- ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
320
+ ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
302
321
 
303
- }
304
- }
322
+ // ensure that the index is set to zero for tagging variants
323
+ // that where first option is auto-selected
324
+ if ( ctrl.activeIndex === -1 && ctrl.taggingLabel !== false ) {
325
+ ctrl.activeIndex = 0;
326
+ }
305
327
 
328
+ // Give it time to appear before focus
329
+ $timeout(function() {
330
+ ctrl.search = initSearchValue || ctrl.search;
331
+ ctrl.searchInput[0].focus();
306
332
  });
333
+ }
334
+ };
307
335
 
308
- if (ctrl.multiple){
309
- //Remove already selected items
310
- $scope.$watchCollection('$select.selected', function(selectedItems){
311
- var data = ctrl.parserResult.source($scope);
312
- if (!selectedItems.length) {
313
- setItemsFn(data);
314
- }else{
315
- if ( data !== undefined ) {
316
- var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
317
- setItemsFn(filteredItems);
318
- }
319
- }
320
- ctrl.sizeSearchInput();
321
- });
336
+ ctrl.findGroupByName = function(name) {
337
+ return ctrl.groups && ctrl.groups.filter(function(group) {
338
+ return group.name === name;
339
+ })[0];
340
+ };
341
+
342
+ ctrl.parseRepeatAttr = function(repeatAttr, groupByExp, groupFilterExp) {
343
+ function updateGroups(items) {
344
+ var groupFn = $scope.$eval(groupByExp);
345
+ ctrl.groups = [];
346
+ angular.forEach(items, function(item) {
347
+ var groupName = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
348
+ var group = ctrl.findGroupByName(groupName);
349
+ if(group) {
350
+ group.items.push(item);
351
+ }
352
+ else {
353
+ ctrl.groups.push({name: groupName, items: [item]});
354
+ }
355
+ });
356
+ if(groupFilterExp){
357
+ var groupFilterFn = $scope.$eval(groupFilterExp);
358
+ if( angular.isFunction(groupFilterFn)){
359
+ ctrl.groups = groupFilterFn(ctrl.groups);
360
+ } else if(angular.isArray(groupFilterFn)){
361
+ ctrl.groups = _groupsFilter(ctrl.groups, groupFilterFn);
362
+ }
322
363
  }
364
+ ctrl.items = [];
365
+ ctrl.groups.forEach(function(group) {
366
+ ctrl.items = ctrl.items.concat(group.items);
367
+ });
368
+ }
323
369
 
324
- };
370
+ function setPlainItems(items) {
371
+ ctrl.items = items;
372
+ }
373
+
374
+ ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;
325
375
 
326
- var _refreshDelayPromise;
327
-
328
- /**
329
- * Typeahead mode: lets the user refresh the collection using his own function.
330
- *
331
- * See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
332
- */
333
- ctrl.refresh = function(refreshAttr) {
334
- if (refreshAttr !== undefined) {
335
-
336
- // Debounce
337
- // See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155
338
- // FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177
339
- if (_refreshDelayPromise) {
340
- $timeout.cancel(_refreshDelayPromise);
376
+ ctrl.parserResult = RepeatParser.parse(repeatAttr);
377
+
378
+ ctrl.isGrouped = !!groupByExp;
379
+ ctrl.itemProperty = ctrl.parserResult.itemName;
380
+
381
+ ctrl.refreshItems = function (data){
382
+ data = data || ctrl.parserResult.source($scope);
383
+ var selectedItems = ctrl.selected;
384
+ //TODO should implement for single mode removeSelected
385
+ if ((angular.isArray(selectedItems) && !selectedItems.length) || !ctrl.removeSelected) {
386
+ ctrl.setItemsFn(data);
387
+ }else{
388
+ if ( data !== undefined ) {
389
+ var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
390
+ ctrl.setItemsFn(filteredItems);
341
391
  }
342
- _refreshDelayPromise = $timeout(function() {
343
- $scope.$eval(refreshAttr);
344
- }, ctrl.refreshDelay);
345
392
  }
346
393
  };
347
394
 
348
- ctrl.setActiveItem = function(item) {
349
- ctrl.activeIndex = ctrl.items.indexOf(item);
350
- };
351
-
352
- ctrl.isActive = function(itemScope) {
353
- if ( !ctrl.open ) {
354
- return false;
395
+ // See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
396
+ $scope.$watchCollection(ctrl.parserResult.source, function(items) {
397
+ if (items === undefined || items === null) {
398
+ // If the user specifies undefined or null => reset the collection
399
+ // Special case: items can be undefined if the user did not initialized the collection on the scope
400
+ // i.e $scope.addresses = [] is missing
401
+ ctrl.items = [];
402
+ } else {
403
+ if (!angular.isArray(items)) {
404
+ throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
405
+ } else {
406
+ //Remove already selected items (ex: while searching)
407
+ //TODO Should add a test
408
+ ctrl.refreshItems(items);
409
+ ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
410
+ }
355
411
  }
356
- var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
357
- var isActive = itemIndex === ctrl.activeIndex;
412
+ });
358
413
 
359
- if ( !isActive || ( itemIndex < 0 && ctrl.taggingLabel !== false ) ||( itemIndex < 0 && ctrl.taggingLabel === false) ) {
360
- return false;
361
- }
414
+ };
415
+
416
+ var _refreshDelayPromise;
362
417
 
363
- if (isActive && !angular.isUndefined(ctrl.onHighlightCallback)) {
364
- itemScope.$eval(ctrl.onHighlightCallback);
418
+ /**
419
+ * Typeahead mode: lets the user refresh the collection using his own function.
420
+ *
421
+ * See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
422
+ */
423
+ ctrl.refresh = function(refreshAttr) {
424
+ if (refreshAttr !== undefined) {
425
+
426
+ // Debounce
427
+ // See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155
428
+ // FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177
429
+ if (_refreshDelayPromise) {
430
+ $timeout.cancel(_refreshDelayPromise);
365
431
  }
432
+ _refreshDelayPromise = $timeout(function() {
433
+ $scope.$eval(refreshAttr);
434
+ }, ctrl.refreshDelay);
435
+ }
436
+ };
366
437
 
367
- return isActive;
368
- };
438
+ ctrl.setActiveItem = function(item) {
439
+ ctrl.activeIndex = ctrl.items.indexOf(item);
440
+ };
369
441
 
370
- ctrl.isDisabled = function(itemScope) {
442
+ ctrl.isActive = function(itemScope) {
443
+ if ( !ctrl.open ) {
444
+ return false;
445
+ }
446
+ var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
447
+ var isActive = itemIndex === ctrl.activeIndex;
371
448
 
372
- if (!ctrl.open) return;
449
+ if ( !isActive || ( itemIndex < 0 && ctrl.taggingLabel !== false ) ||( itemIndex < 0 && ctrl.taggingLabel === false) ) {
450
+ return false;
451
+ }
373
452
 
374
- var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
375
- var isDisabled = false;
376
- var item;
453
+ if (isActive && !angular.isUndefined(ctrl.onHighlightCallback)) {
454
+ itemScope.$eval(ctrl.onHighlightCallback);
455
+ }
377
456
 
378
- if (itemIndex >= 0 && !angular.isUndefined(ctrl.disableChoiceExpression)) {
379
- item = ctrl.items[itemIndex];
380
- isDisabled = !!(itemScope.$eval(ctrl.disableChoiceExpression)); // force the boolean value
381
- item._uiSelectChoiceDisabled = isDisabled; // store this for later reference
382
- }
457
+ return isActive;
458
+ };
383
459
 
384
- return isDisabled;
385
- };
460
+ ctrl.isDisabled = function(itemScope) {
386
461
 
462
+ if (!ctrl.open) return;
387
463
 
388
- // When the user selects an item with ENTER or clicks the dropdown
389
- ctrl.select = function(item, skipFocusser, $event) {
390
- if (item === undefined || !item._uiSelectChoiceDisabled) {
464
+ var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
465
+ var isDisabled = false;
466
+ var item;
391
467
 
392
- if ( ! ctrl.items && ! ctrl.search ) return;
468
+ if (itemIndex >= 0 && !angular.isUndefined(ctrl.disableChoiceExpression)) {
469
+ item = ctrl.items[itemIndex];
470
+ isDisabled = !!(itemScope.$eval(ctrl.disableChoiceExpression)); // force the boolean value
471
+ item._uiSelectChoiceDisabled = isDisabled; // store this for later reference
472
+ }
393
473
 
394
- if (!item || !item._uiSelectChoiceDisabled) {
395
- if(ctrl.tagging.isActivated) {
396
- // if taggingLabel is disabled, we pull from ctrl.search val
397
- if ( ctrl.taggingLabel === false ) {
398
- if ( ctrl.activeIndex < 0 ) {
399
- item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search;
400
- if ( angular.equals( ctrl.items[0], item ) ) {
401
- return;
402
- }
403
- } else {
404
- // keyboard nav happened first, user selected from dropdown
405
- item = ctrl.items[ctrl.activeIndex];
474
+ return isDisabled;
475
+ };
476
+
477
+
478
+ // When the user selects an item with ENTER or clicks the dropdown
479
+ ctrl.select = function(item, skipFocusser, $event) {
480
+ if (item === undefined || !item._uiSelectChoiceDisabled) {
481
+
482
+ if ( ! ctrl.items && ! ctrl.search ) return;
483
+
484
+ if (!item || !item._uiSelectChoiceDisabled) {
485
+ if(ctrl.tagging.isActivated) {
486
+ // if taggingLabel is disabled, we pull from ctrl.search val
487
+ if ( ctrl.taggingLabel === false ) {
488
+ if ( ctrl.activeIndex < 0 ) {
489
+ item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search;
490
+ if (!item || angular.equals( ctrl.items[0], item ) ) {
491
+ return;
406
492
  }
407
493
  } else {
408
- // tagging always operates at index zero, taggingLabel === false pushes
409
- // the ctrl.search value without having it injected
410
- if ( ctrl.activeIndex === 0 ) {
411
- // ctrl.tagging pushes items to ctrl.items, so we only have empty val
412
- // for `item` if it is a detected duplicate
413
- if ( item === undefined ) return;
414
-
415
- // create new item on the fly if we don't already have one;
416
- // use tagging function if we have one
417
- if ( ctrl.tagging.fct !== undefined && typeof item === 'string' ) {
418
- item = ctrl.tagging.fct(ctrl.search);
419
- // if item type is 'string', apply the tagging label
420
- } else if ( typeof item === 'string' ) {
421
- item = item.replace(ctrl.taggingLabel,'');
422
- }
423
- }
494
+ // keyboard nav happened first, user selected from dropdown
495
+ item = ctrl.items[ctrl.activeIndex];
424
496
  }
425
- // search ctrl.selected for dupes potentially caused by tagging and return early if found
426
- if ( ctrl.selected && ctrl.selected.filter( function (selection) { return angular.equals(selection, item); }).length > 0 ) {
427
- ctrl.close(skipFocusser);
428
- return;
497
+ } else {
498
+ // tagging always operates at index zero, taggingLabel === false pushes
499
+ // the ctrl.search value without having it injected
500
+ if ( ctrl.activeIndex === 0 ) {
501
+ // ctrl.tagging pushes items to ctrl.items, so we only have empty val
502
+ // for `item` if it is a detected duplicate
503
+ if ( item === undefined ) return;
504
+
505
+ // create new item on the fly if we don't already have one;
506
+ // use tagging function if we have one
507
+ if ( ctrl.tagging.fct !== undefined && typeof item === 'string' ) {
508
+ item = ctrl.tagging.fct(ctrl.search);
509
+ if (!item) return;
510
+ // if item type is 'string', apply the tagging label
511
+ } else if ( typeof item === 'string' ) {
512
+ // trim the trailing space
513
+ item = item.replace(ctrl.taggingLabel,'').trim();
514
+ }
429
515
  }
430
516
  }
517
+ // search ctrl.selected for dupes potentially caused by tagging and return early if found
518
+ if ( ctrl.selected && angular.isArray(ctrl.selected) && ctrl.selected.filter( function (selection) { return angular.equals(selection, item); }).length > 0 ) {
519
+ ctrl.close(skipFocusser);
520
+ return;
521
+ }
522
+ }
431
523
 
432
- var locals = {};
433
- locals[ctrl.parserResult.itemName] = item;
524
+ $scope.$broadcast('uis:select', item);
434
525
 
435
- if(ctrl.multiple) {
436
- ctrl.selected.push(item);
437
- ctrl.sizeSearchInput();
438
- } else {
439
- ctrl.selected = item;
440
- }
526
+ var locals = {};
527
+ locals[ctrl.parserResult.itemName] = item;
441
528
 
529
+ $timeout(function(){
442
530
  ctrl.onSelectCallback($scope, {
443
- $item: item,
444
- $model: ctrl.parserResult.modelMapper($scope, locals)
531
+ $item: item,
532
+ $model: ctrl.parserResult.modelMapper($scope, locals)
445
533
  });
534
+ });
446
535
 
447
- if (!ctrl.multiple || ctrl.closeOnSelect) {
448
- ctrl.close(skipFocusser);
449
- }
450
- if ($event && $event.type === 'click') {
451
- ctrl.clickTriggeredSelect = true;
452
- }
536
+ if (ctrl.closeOnSelect) {
537
+ ctrl.close(skipFocusser);
538
+ }
539
+ if ($event && $event.type === 'click') {
540
+ ctrl.clickTriggeredSelect = true;
453
541
  }
454
542
  }
455
- };
456
-
457
- // Closes the dropdown
458
- ctrl.close = function(skipFocusser) {
459
- if (!ctrl.open) return;
460
- _resetSearchInput();
461
- ctrl.open = false;
462
- if (!ctrl.multiple){
463
- $timeout(function(){
464
- ctrl.focusser.prop('disabled', false);
465
- if (!skipFocusser) ctrl.focusser[0].focus();
466
- },0,false);
467
- }
468
- };
469
-
470
- // Toggle dropdown
471
- ctrl.toggle = function(e) {
472
- if (ctrl.open) {
473
- ctrl.close();
474
- e.preventDefault();
475
- e.stopPropagation();
476
- } else {
477
- ctrl.activate();
478
- }
479
- };
543
+ }
544
+ };
480
545
 
481
- ctrl.isLocked = function(itemScope, itemIndex) {
482
- var isLocked, item = ctrl.selected[itemIndex];
546
+ // Closes the dropdown
547
+ ctrl.close = function(skipFocusser) {
548
+ if (!ctrl.open) return;
549
+ if (ctrl.ngModel && ctrl.ngModel.$setTouched) ctrl.ngModel.$setTouched();
550
+ _resetSearchInput();
551
+ ctrl.open = false;
483
552
 
484
- if (item && !angular.isUndefined(ctrl.lockChoiceExpression)) {
485
- isLocked = !!(itemScope.$eval(ctrl.lockChoiceExpression)); // force the boolean value
486
- item._uiSelectChoiceLocked = isLocked; // store this for later reference
487
- }
553
+ $scope.$broadcast('uis:close', skipFocusser);
488
554
 
489
- return isLocked;
490
- };
555
+ };
491
556
 
492
- // Remove item from multiple select
493
- ctrl.removeChoice = function(index){
494
- var removedChoice = ctrl.selected[index];
557
+ ctrl.setFocus = function(){
558
+ if (!ctrl.focus) ctrl.focusInput[0].focus();
559
+ };
495
560
 
496
- // if the choice is locked, can't remove it
497
- if(removedChoice._uiSelectChoiceLocked) return;
561
+ ctrl.clear = function($event) {
562
+ ctrl.select(undefined);
563
+ $event.stopPropagation();
564
+ $timeout(function() {
565
+ ctrl.focusser[0].focus();
566
+ }, 0, false);
567
+ };
498
568
 
499
- var locals = {};
500
- locals[ctrl.parserResult.itemName] = removedChoice;
569
+ // Toggle dropdown
570
+ ctrl.toggle = function(e) {
571
+ if (ctrl.open) {
572
+ ctrl.close();
573
+ e.preventDefault();
574
+ e.stopPropagation();
575
+ } else {
576
+ ctrl.activate();
577
+ }
578
+ };
501
579
 
502
- ctrl.selected.splice(index, 1);
503
- ctrl.activeMatchIndex = -1;
504
- ctrl.sizeSearchInput();
580
+ ctrl.isLocked = function(itemScope, itemIndex) {
581
+ var isLocked, item = ctrl.selected[itemIndex];
505
582
 
506
- ctrl.onRemoveCallback($scope, {
507
- $item: removedChoice,
508
- $model: ctrl.parserResult.modelMapper($scope, locals)
509
- });
510
- };
583
+ if (item && !angular.isUndefined(ctrl.lockChoiceExpression)) {
584
+ isLocked = !!(itemScope.$eval(ctrl.lockChoiceExpression)); // force the boolean value
585
+ item._uiSelectChoiceLocked = isLocked; // store this for later reference
586
+ }
511
587
 
512
- ctrl.getPlaceholder = function(){
513
- //Refactor single?
514
- if(ctrl.multiple && ctrl.selected.length) return;
515
- return ctrl.placeholder;
516
- };
588
+ return isLocked;
589
+ };
517
590
 
518
- var containerSizeWatch;
519
- ctrl.sizeSearchInput = function(){
520
- var input = _searchInput[0],
521
- container = _searchInput.parent().parent()[0];
522
- _searchInput.css('width','10px');
523
- var calculate = function(){
524
- var newWidth = container.clientWidth - input.offsetLeft - 10;
525
- if(newWidth < 50) newWidth = container.clientWidth;
526
- _searchInput.css('width',newWidth+'px');
527
- };
528
- $timeout(function(){ //Give tags time to render correctly
529
- if (container.clientWidth === 0 && !containerSizeWatch){
530
- containerSizeWatch = $scope.$watch(function(){ return container.clientWidth;}, function(newValue){
531
- if (newValue !== 0){
532
- calculate();
533
- containerSizeWatch();
534
- containerSizeWatch = null;
535
- }
536
- });
537
- }else if (!containerSizeWatch) {
538
- calculate();
539
- }
540
- }, 0, false);
541
- };
591
+ var sizeWatch = null;
592
+ ctrl.sizeSearchInput = function() {
593
+
594
+ var input = ctrl.searchInput[0],
595
+ container = ctrl.searchInput.parent().parent()[0],
596
+ calculateContainerWidth = function() {
597
+ // Return the container width only if the search input is visible
598
+ return container.clientWidth * !!input.offsetParent;
599
+ },
600
+ updateIfVisible = function(containerWidth) {
601
+ if (containerWidth === 0) {
602
+ return false;
603
+ }
604
+ var inputWidth = containerWidth - input.offsetLeft - 10;
605
+ if (inputWidth < 50) inputWidth = containerWidth;
606
+ ctrl.searchInput.css('width', inputWidth+'px');
607
+ return true;
608
+ };
542
609
 
543
- function _handleDropDownSelection(key) {
544
- var processed = true;
545
- switch (key) {
546
- case KEY.DOWN:
547
- if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
548
- else if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
549
- break;
550
- case KEY.UP:
551
- if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
552
- else if (ctrl.activeIndex > 0 || (ctrl.search.length === 0 && ctrl.tagging.isActivated)) { ctrl.activeIndex--; }
553
- break;
554
- case KEY.TAB:
555
- if (!ctrl.multiple || ctrl.open) ctrl.select(ctrl.items[ctrl.activeIndex], true);
556
- break;
557
- case KEY.ENTER:
558
- if(ctrl.open){
559
- ctrl.select(ctrl.items[ctrl.activeIndex]);
560
- } else {
561
- ctrl.activate(false, true); //In case its the search input in 'multiple' mode
610
+ ctrl.searchInput.css('width', '10px');
611
+ $timeout(function() { //Give tags time to render correctly
612
+ if (sizeWatch === null && !updateIfVisible(calculateContainerWidth())) {
613
+ sizeWatch = $scope.$watch(calculateContainerWidth, function(containerWidth) {
614
+ if (updateIfVisible(containerWidth)) {
615
+ sizeWatch();
616
+ sizeWatch = null;
562
617
  }
563
- break;
564
- case KEY.ESC:
565
- ctrl.close();
566
- break;
567
- default:
568
- processed = false;
618
+ });
569
619
  }
570
- return processed;
571
- }
572
-
573
- // Handles selected options in "multiple" mode
574
- function _handleMatchSelection(key){
575
- var caretPosition = _getCaretPosition(_searchInput[0]),
576
- length = ctrl.selected.length,
577
- // none = -1,
578
- first = 0,
579
- last = length-1,
580
- curr = ctrl.activeMatchIndex,
581
- next = ctrl.activeMatchIndex+1,
582
- prev = ctrl.activeMatchIndex-1,
583
- newIndex = curr;
584
-
585
- if(caretPosition > 0 || (ctrl.search.length && key == KEY.RIGHT)) return false;
586
-
587
- ctrl.close();
620
+ });
621
+ };
588
622
 
589
- function getNewActiveMatchIndex(){
590
- switch(key){
591
- case KEY.LEFT:
592
- // Select previous/first item
593
- if(~ctrl.activeMatchIndex) return prev;
594
- // Select last item
595
- else return last;
596
- break;
597
- case KEY.RIGHT:
598
- // Open drop-down
599
- if(!~ctrl.activeMatchIndex || curr === last){
600
- ctrl.activate();
601
- return false;
602
- }
603
- // Select next/last item
604
- else return next;
605
- break;
606
- case KEY.BACKSPACE:
607
- // Remove selected item and select previous/first
608
- if(~ctrl.activeMatchIndex){
609
- ctrl.removeChoice(curr);
610
- return prev;
611
- }
612
- // Select last item
613
- else return last;
614
- break;
615
- case KEY.DELETE:
616
- // Remove selected item and select next item
617
- if(~ctrl.activeMatchIndex){
618
- ctrl.removeChoice(ctrl.activeMatchIndex);
619
- return curr;
620
- }
621
- else return false;
623
+ function _handleDropDownSelection(key) {
624
+ var processed = true;
625
+ switch (key) {
626
+ case KEY.DOWN:
627
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
628
+ else if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
629
+ break;
630
+ case KEY.UP:
631
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
632
+ else if (ctrl.activeIndex > 0 || (ctrl.search.length === 0 && ctrl.tagging.isActivated && ctrl.activeIndex > -1)) { ctrl.activeIndex--; }
633
+ break;
634
+ case KEY.TAB:
635
+ if (!ctrl.multiple || ctrl.open) ctrl.select(ctrl.items[ctrl.activeIndex], true);
636
+ break;
637
+ case KEY.ENTER:
638
+ if(ctrl.open && (ctrl.tagging.isActivated || ctrl.activeIndex >= 0)){
639
+ ctrl.select(ctrl.items[ctrl.activeIndex]); // Make sure at least one dropdown item is highlighted before adding if not in tagging mode
640
+ } else {
641
+ ctrl.activate(false, true); //In case its the search input in 'multiple' mode
622
642
  }
623
- }
624
-
625
- newIndex = getNewActiveMatchIndex();
626
-
627
- if(!ctrl.selected.length || newIndex === false) ctrl.activeMatchIndex = -1;
628
- else ctrl.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
629
-
630
- return true;
643
+ break;
644
+ case KEY.ESC:
645
+ ctrl.close();
646
+ break;
647
+ default:
648
+ processed = false;
631
649
  }
650
+ return processed;
651
+ }
632
652
 
633
- // Bind to keyboard shortcuts
634
- _searchInput.on('keydown', function(e) {
653
+ // Bind to keyboard shortcuts
654
+ ctrl.searchInput.on('keydown', function(e) {
635
655
 
636
- var key = e.which;
656
+ var key = e.which;
637
657
 
638
- // if(~[KEY.ESC,KEY.TAB].indexOf(key)){
639
- // //TODO: SEGURO?
640
- // ctrl.close();
641
- // }
658
+ // if(~[KEY.ESC,KEY.TAB].indexOf(key)){
659
+ // //TODO: SEGURO?
660
+ // ctrl.close();
661
+ // }
642
662
 
643
- $scope.$apply(function() {
644
- var processed = false;
645
- var tagged = false;
663
+ $scope.$apply(function() {
646
664
 
647
- if(ctrl.multiple && KEY.isHorizontalMovement(key)){
648
- processed = _handleMatchSelection(key);
649
- }
665
+ var tagged = false;
650
666
 
651
- if (!processed && (ctrl.items.length > 0 || ctrl.tagging.isActivated)) {
652
- processed = _handleDropDownSelection(key);
653
- if ( ctrl.taggingTokens.isActivated ) {
654
- for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) {
655
- if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) {
656
- // make sure there is a new value to push via tagging
657
- if ( ctrl.search.length > 0 ) {
658
- tagged = true;
659
- }
667
+ if (ctrl.items.length > 0 || ctrl.tagging.isActivated) {
668
+ _handleDropDownSelection(key);
669
+ if ( ctrl.taggingTokens.isActivated ) {
670
+ for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) {
671
+ if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) {
672
+ // make sure there is a new value to push via tagging
673
+ if ( ctrl.search.length > 0 ) {
674
+ tagged = true;
660
675
  }
661
676
  }
662
- if ( tagged ) {
663
- $timeout(function() {
664
- _searchInput.triggerHandler('tagged');
665
- var newItem = ctrl.search.replace(KEY.MAP[e.keyCode],'').trim();
666
- if ( ctrl.tagging.fct ) {
667
- newItem = ctrl.tagging.fct( newItem );
668
- }
669
- ctrl.select( newItem, true);
670
- });
671
- }
677
+ }
678
+ if ( tagged ) {
679
+ $timeout(function() {
680
+ ctrl.searchInput.triggerHandler('tagged');
681
+ var newItem = ctrl.search.replace(KEY.MAP[e.keyCode],'').trim();
682
+ if ( ctrl.tagging.fct ) {
683
+ newItem = ctrl.tagging.fct( newItem );
684
+ }
685
+ if (newItem) ctrl.select(newItem, true);
686
+ });
672
687
  }
673
688
  }
674
-
675
- if (processed && key != KEY.TAB) {
676
- //TODO Check si el tab selecciona aun correctamente
677
- //Crear test
678
- e.preventDefault();
679
- e.stopPropagation();
680
- }
681
- });
682
-
683
- if(KEY.isVerticalMovement(key) && ctrl.items.length > 0){
684
- _ensureHighlightVisible();
685
689
  }
686
690
 
687
691
  });
688
692
 
689
- _searchInput.on('keyup', function(e) {
690
- if ( ! KEY.isVerticalMovement(e.which) ) {
691
- $scope.$evalAsync( function () {
692
- ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0;
693
- });
694
- }
695
- // Push a "create new" item into array if there is a search string
696
- if ( ctrl.tagging.isActivated && ctrl.search.length > 0 ) {
693
+ if(KEY.isVerticalMovement(key) && ctrl.items.length > 0){
694
+ _ensureHighlightVisible();
695
+ }
697
696
 
698
- // return early with these keys
699
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
700
- return;
701
- }
702
- // always reset the activeIndex to the first item when tagging
703
- ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0;
704
- // taggingLabel === false bypasses all of this
705
- if (ctrl.taggingLabel === false) return;
706
-
707
- var items = angular.copy( ctrl.items );
708
- var stashArr = angular.copy( ctrl.items );
709
- var newItem;
710
- var item;
711
- var hasTag = false;
712
- var dupeIndex = -1;
713
- var tagItems;
714
- var tagItem;
715
-
716
- // case for object tagging via transform `ctrl.tagging.fct` function
717
- if ( ctrl.tagging.fct !== undefined) {
718
- tagItems = ctrl.$filter('filter')(items,{'isTag': true});
719
- if ( tagItems.length > 0 ) {
720
- tagItem = tagItems[0];
721
- }
722
- // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
723
- if ( items.length > 0 && tagItem ) {
724
- hasTag = true;
725
- items = items.slice(1,items.length);
726
- stashArr = stashArr.slice(1,stashArr.length);
727
- }
728
- newItem = ctrl.tagging.fct(ctrl.search);
729
- newItem.isTag = true;
730
- // verify the the tag doesn't match the value of an existing item
731
- if ( stashArr.filter( function (origItem) { return angular.equals( origItem, ctrl.tagging.fct(ctrl.search) ); } ).length > 0 ) {
732
- return;
733
- }
734
- // handle newItem string and stripping dupes in tagging string context
735
- } else {
736
- // find any tagging items already in the ctrl.items array and store them
737
- tagItems = ctrl.$filter('filter')(items,function (item) {
738
- return item.match(ctrl.taggingLabel);
739
- });
740
- if ( tagItems.length > 0 ) {
741
- tagItem = tagItems[0];
742
- }
743
- item = items[0];
744
- // remove existing tag item if found (should only ever be one tag item)
745
- if ( item !== undefined && items.length > 0 && tagItem ) {
746
- hasTag = true;
747
- items = items.slice(1,items.length);
748
- stashArr = stashArr.slice(1,stashArr.length);
749
- }
750
- newItem = ctrl.search+' '+ctrl.taggingLabel;
751
- if ( _findApproxDupe(ctrl.selected, ctrl.search) > -1 ) {
752
- return;
753
- }
754
- // verify the the tag doesn't match the value of an existing item from
755
- // the searched data set or the items already selected
756
- if ( _findCaseInsensitiveDupe(stashArr.concat(ctrl.selected)) ) {
757
- // if there is a tag from prev iteration, strip it / queue the change
758
- // and return early
759
- if ( hasTag ) {
760
- items = stashArr;
761
- $scope.$evalAsync( function () {
762
- ctrl.activeIndex = 0;
763
- ctrl.items = items;
764
- });
765
- }
766
- return;
767
- }
768
- if ( _findCaseInsensitiveDupe(stashArr) ) {
769
- // if there is a tag from prev iteration, strip it
770
- if ( hasTag ) {
771
- ctrl.items = stashArr.slice(1,stashArr.length);
772
- }
773
- return;
697
+ if (key === KEY.ENTER || key === KEY.ESC) {
698
+ e.preventDefault();
699
+ e.stopPropagation();
700
+ }
701
+
702
+ });
703
+
704
+ // If tagging try to split by tokens and add items
705
+ ctrl.searchInput.on('paste', function (e) {
706
+ var data = e.originalEvent.clipboardData.getData('text/plain');
707
+ if (data && data.length > 0 && ctrl.taggingTokens.isActivated && ctrl.tagging.fct) {
708
+ var items = data.split(ctrl.taggingTokens.tokens[0]); // split by first token only
709
+ if (items && items.length > 0) {
710
+ angular.forEach(items, function (item) {
711
+ var newItem = ctrl.tagging.fct(item);
712
+ if (newItem) {
713
+ ctrl.select(newItem, true);
774
714
  }
775
- }
776
- if ( hasTag ) dupeIndex = _findApproxDupe(ctrl.selected, newItem);
777
- // dupe found, shave the first item
778
- if ( dupeIndex > -1 ) {
779
- items = items.slice(dupeIndex+1,items.length-1);
780
- } else {
781
- items = [];
782
- items.push(newItem);
783
- items = items.concat(stashArr);
784
- }
785
- $scope.$evalAsync( function () {
786
- ctrl.activeIndex = 0;
787
- ctrl.items = items;
788
715
  });
716
+ e.preventDefault();
717
+ e.stopPropagation();
789
718
  }
719
+ }
720
+ });
721
+
722
+ ctrl.searchInput.on('tagged', function() {
723
+ $timeout(function() {
724
+ _resetSearchInput();
790
725
  });
726
+ });
791
727
 
792
- _searchInput.on('tagged', function() {
793
- $timeout(function() {
794
- _resetSearchInput();
795
- });
796
- });
797
-
798
- _searchInput.on('blur', function() {
799
- $timeout(function() {
800
- ctrl.activeMatchIndex = -1;
801
- });
802
- });
803
-
804
- function _findCaseInsensitiveDupe(arr) {
805
- if ( arr === undefined || ctrl.search === undefined ) {
806
- return false;
807
- }
808
- var hasDupe = arr.filter( function (origItem) {
809
- if ( ctrl.search.toUpperCase() === undefined ) {
810
- return false;
811
- }
812
- return origItem.toUpperCase() === ctrl.search.toUpperCase();
813
- }).length > 0;
814
-
815
- return hasDupe;
728
+ // See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
729
+ function _ensureHighlightVisible() {
730
+ var container = $element.querySelectorAll('.ui-select-choices-content');
731
+ var choices = container.querySelectorAll('.ui-select-choices-row');
732
+ if (choices.length < 1) {
733
+ throw uiSelectMinErr('choices', "Expected multiple .ui-select-choices-row but got '{0}'.", choices.length);
816
734
  }
817
735
 
818
- function _findApproxDupe(haystack, needle) {
819
- var tempArr = angular.copy(haystack);
820
- var dupeIndex = -1;
821
- for (var i = 0; i <tempArr.length; i++) {
822
- // handle the simple string version of tagging
823
- if ( ctrl.tagging.fct === undefined ) {
824
- // search the array for the match
825
- if ( tempArr[i]+' '+ctrl.taggingLabel === needle ) {
826
- dupeIndex = i;
827
- }
828
- // handle the object tagging implementation
829
- } else {
830
- var mockObj = tempArr[i];
831
- mockObj.isTag = true;
832
- if ( angular.equals(mockObj, needle) ) {
833
- dupeIndex = i;
834
- }
835
- }
836
- }
837
- return dupeIndex;
736
+ if (ctrl.activeIndex < 0) {
737
+ return;
838
738
  }
839
739
 
840
- function _getCaretPosition(el) {
841
- if(angular.isNumber(el.selectionStart)) return el.selectionStart;
842
- // selectionStart is not supported in IE8 and we don't want hacky workarounds so we compromise
843
- else return el.value.length;
740
+ var highlighted = choices[ctrl.activeIndex];
741
+ var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
742
+ var height = container[0].offsetHeight;
743
+
744
+ if (posY > height) {
745
+ container[0].scrollTop += posY - height;
746
+ } else if (posY < highlighted.clientHeight) {
747
+ if (ctrl.isGrouped && ctrl.activeIndex === 0)
748
+ container[0].scrollTop = 0; //To make group header visible when going all the way up
749
+ else
750
+ container[0].scrollTop -= highlighted.clientHeight - posY;
844
751
  }
752
+ }
845
753
 
846
- // See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
847
- function _ensureHighlightVisible() {
848
- var container = $element.querySelectorAll('.ui-select-choices-content');
849
- var choices = container.querySelectorAll('.ui-select-choices-row');
850
- if (choices.length < 1) {
851
- throw uiSelectMinErr('choices', "Expected multiple .ui-select-choices-row but got '{0}'.", choices.length);
852
- }
754
+ $scope.$on('$destroy', function() {
755
+ ctrl.searchInput.off('keyup keydown tagged blur paste');
756
+ });
853
757
 
854
- var highlighted = choices[ctrl.activeIndex];
855
- var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
856
- var height = container[0].offsetHeight;
857
-
858
- if (posY > height) {
859
- container[0].scrollTop += posY - height;
860
- } else if (posY < highlighted.clientHeight) {
861
- if (ctrl.isGrouped && ctrl.activeIndex === 0)
862
- container[0].scrollTop = 0; //To make group header visible when going all the way up
863
- else
864
- container[0].scrollTop -= highlighted.clientHeight - posY;
865
- }
866
- }
758
+ }]);
867
759
 
868
- $scope.$on('$destroy', function() {
869
- _searchInput.off('keyup keydown tagged blur');
870
- });
871
- }])
760
+ uis.directive('uiSelect',
761
+ ['$document', 'uiSelectConfig', 'uiSelectMinErr', 'uisOffset', '$compile', '$parse', '$timeout',
762
+ function($document, uiSelectConfig, uiSelectMinErr, uisOffset, $compile, $parse, $timeout) {
872
763
 
873
- .directive('uiSelect',
874
- ['$document', 'uiSelectConfig', 'uiSelectMinErr', '$compile', '$parse',
875
- function($document, uiSelectConfig, uiSelectMinErr, $compile, $parse) {
764
+ return {
765
+ restrict: 'EA',
766
+ templateUrl: function(tElement, tAttrs) {
767
+ var theme = tAttrs.theme || uiSelectConfig.theme;
768
+ return theme + (angular.isDefined(tAttrs.multiple) ? '/select-multiple.tpl.html' : '/select.tpl.html');
769
+ },
770
+ replace: true,
771
+ transclude: true,
772
+ require: ['uiSelect', '^ngModel'],
773
+ scope: true,
774
+
775
+ controller: 'uiSelectCtrl',
776
+ controllerAs: '$select',
777
+ compile: function(tElement, tAttrs) {
778
+
779
+ //Multiple or Single depending if multiple attribute presence
780
+ if (angular.isDefined(tAttrs.multiple))
781
+ tElement.append("<ui-select-multiple/>").removeAttr('multiple');
782
+ else
783
+ tElement.append("<ui-select-single/>");
784
+
785
+ return function(scope, element, attrs, ctrls, transcludeFn) {
876
786
 
877
- return {
878
- restrict: 'EA',
879
- templateUrl: function(tElement, tAttrs) {
880
- var theme = tAttrs.theme || uiSelectConfig.theme;
881
- return theme + (angular.isDefined(tAttrs.multiple) ? '/select-multiple.tpl.html' : '/select.tpl.html');
882
- },
883
- replace: true,
884
- transclude: true,
885
- require: ['uiSelect', 'ngModel'],
886
- scope: true,
887
-
888
- controller: 'uiSelectCtrl',
889
- controllerAs: '$select',
890
-
891
- link: function(scope, element, attrs, ctrls, transcludeFn) {
892
787
  var $select = ctrls[0];
893
788
  var ngModel = ctrls[1];
894
789
 
895
- var searchInput = element.querySelectorAll('input.ui-select-search');
896
-
897
- $select.multiple = angular.isDefined(attrs.multiple) && (
898
- attrs.multiple === '' ||
899
- attrs.multiple.toLowerCase() === 'multiple' ||
900
- attrs.multiple.toLowerCase() === 'true'
901
- );
790
+ $select.generatedId = uiSelectConfig.generateId();
791
+ $select.baseTitle = attrs.title || 'Select box';
792
+ $select.focusserTitle = $select.baseTitle + ' focus';
793
+ $select.focusserId = 'focusser-' + $select.generatedId;
902
794
 
903
795
  $select.closeOnSelect = function() {
904
796
  if (angular.isDefined(attrs.closeOnSelect)) {
@@ -910,73 +802,7 @@
910
802
 
911
803
  $select.onSelectCallback = $parse(attrs.onSelect);
912
804
  $select.onRemoveCallback = $parse(attrs.onRemove);
913
-
914
- //From view --> model
915
- ngModel.$parsers.unshift(function (inputValue) {
916
- var locals = {},
917
- result;
918
- if ($select.multiple){
919
- var resultMultiple = [];
920
- for (var j = $select.selected.length - 1; j >= 0; j--) {
921
- locals = {};
922
- locals[$select.parserResult.itemName] = $select.selected[j];
923
- result = $select.parserResult.modelMapper(scope, locals);
924
- resultMultiple.unshift(result);
925
- }
926
- return resultMultiple;
927
- }else{
928
- locals = {};
929
- locals[$select.parserResult.itemName] = inputValue;
930
- result = $select.parserResult.modelMapper(scope, locals);
931
- return result;
932
- }
933
- });
934
-
935
- //From model --> view
936
- ngModel.$formatters.unshift(function (inputValue) {
937
- var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
938
- locals = {},
939
- result;
940
- if (data){
941
- if ($select.multiple){
942
- var resultMultiple = [];
943
- var checkFnMultiple = function(list, value){
944
- if (!list || !list.length) return;
945
- for (var p = list.length - 1; p >= 0; p--) {
946
- locals[$select.parserResult.itemName] = list[p];
947
- result = $select.parserResult.modelMapper(scope, locals);
948
- if (result == value){
949
- resultMultiple.unshift(list[p]);
950
- return true;
951
- }
952
- }
953
- return false;
954
- };
955
- if (!inputValue) return resultMultiple; //If ngModel was undefined
956
- for (var k = inputValue.length - 1; k >= 0; k--) {
957
- if (!checkFnMultiple($select.selected, inputValue[k])){
958
- checkFnMultiple(data, inputValue[k]);
959
- }
960
- }
961
- return resultMultiple;
962
- }else{
963
- var checkFnSingle = function(d){
964
- locals[$select.parserResult.itemName] = d;
965
- result = $select.parserResult.modelMapper(scope, locals);
966
- return result == inputValue;
967
- };
968
- //If possible pass same object stored in $select.selected
969
- if ($select.selected && checkFnSingle($select.selected)) {
970
- return $select.selected;
971
- }
972
- for (var i = data.length - 1; i >= 0; i--) {
973
- if (checkFnSingle(data[i])) return data[i];
974
- }
975
- }
976
- }
977
- return inputValue;
978
- });
979
-
805
+
980
806
  //Set reference to ngModel from uiSelectCtrl
981
807
  $select.ngModel = ngModel;
982
808
 
@@ -984,82 +810,23 @@
984
810
  return $select.isGrouped && group && group.name;
985
811
  };
986
812
 
987
- //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
988
- var focusser = angular.element("<input ng-disabled='$select.disabled' class='ui-select-focusser ui-select-offscreen' type='text' aria-haspopup='true' role='button' />");
989
-
990
813
  if(attrs.tabindex){
991
- //tabindex might be an expression, wait until it contains the actual value before we set the focusser tabindex
992
814
  attrs.$observe('tabindex', function(value) {
993
- //If we are using multiple, add tabindex to the search input
994
- if($select.multiple){
995
- searchInput.attr("tabindex", value);
996
- } else {
997
- focusser.attr("tabindex", value);
998
- }
999
- //Remove the tabindex on the parent so that it is not focusable
815
+ $select.focusInput.attr("tabindex", value);
1000
816
  element.removeAttr("tabindex");
1001
817
  });
1002
818
  }
1003
819
 
1004
- $compile(focusser)(scope);
1005
- $select.focusser = focusser;
1006
-
1007
- if (!$select.multiple){
1008
-
1009
- element.append(focusser);
1010
- focusser.bind("focus", function(){
1011
- scope.$evalAsync(function(){
1012
- $select.focus = true;
1013
- });
1014
- });
1015
- focusser.bind("blur", function(){
1016
- scope.$evalAsync(function(){
1017
- $select.focus = false;
1018
- });
1019
- });
1020
- focusser.bind("keydown", function(e){
1021
-
1022
- if (e.which === KEY.BACKSPACE) {
1023
- e.preventDefault();
1024
- e.stopPropagation();
1025
- $select.select(undefined);
1026
- scope.$apply();
1027
- return;
1028
- }
1029
-
1030
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1031
- return;
1032
- }
1033
-
1034
- if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){
1035
- e.preventDefault();
1036
- e.stopPropagation();
1037
- $select.activate();
1038
- }
1039
-
1040
- scope.$digest();
1041
- });
1042
-
1043
- focusser.bind("keyup input", function(e){
1044
-
1045
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
1046
- return;
1047
- }
1048
-
1049
- $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
1050
- focusser.val('');
1051
- scope.$digest();
1052
-
1053
- });
1054
-
1055
- }
1056
-
1057
-
1058
820
  scope.$watch('searchEnabled', function() {
1059
821
  var searchEnabled = scope.$eval(attrs.searchEnabled);
1060
822
  $select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
1061
823
  });
1062
824
 
825
+ scope.$watch('sortable', function() {
826
+ var sortable = scope.$eval(attrs.sortable);
827
+ $select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
828
+ });
829
+
1063
830
  attrs.$observe('disabled', function() {
1064
831
  // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
1065
832
  $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
@@ -1106,39 +873,25 @@
1106
873
  }
1107
874
  });
1108
875
 
1109
- if ($select.multiple){
1110
- scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
1111
- if (oldValue != newValue)
1112
- ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
1113
- });
1114
- scope.$watchCollection('$select.selected', function() {
1115
- ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
1116
- });
1117
- focusser.prop('disabled', true); //Focusser isn't needed if multiple
1118
- }else{
1119
- scope.$watch('$select.selected', function(newValue) {
1120
- if (ngModel.$viewValue !== newValue) {
1121
- ngModel.$setViewValue(newValue);
1122
- }
876
+ //Automatically gets focus when loaded
877
+ if (angular.isDefined(attrs.autofocus)){
878
+ $timeout(function(){
879
+ $select.setFocus();
1123
880
  });
1124
881
  }
1125
882
 
1126
- ngModel.$render = function() {
1127
- if($select.multiple){
1128
- // Make sure that model value is array
1129
- if(!angular.isArray(ngModel.$viewValue)){
1130
- // Have tolerance for null or undefined values
1131
- if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){
1132
- $select.selected = [];
1133
- } else {
1134
- throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
1135
- }
1136
- }
1137
- }
1138
- $select.selected = ngModel.$viewValue;
1139
- };
883
+ //Gets focus based on scope event name (e.g. focus-on='SomeEventName')
884
+ if (angular.isDefined(attrs.focusOn)){
885
+ scope.$on(attrs.focusOn, function() {
886
+ $timeout(function(){
887
+ $select.setFocus();
888
+ });
889
+ });
890
+ }
1140
891
 
1141
892
  function onDocumentClick(e) {
893
+ if (!$select.open) return; //Skip it if dropdown is close
894
+
1142
895
  var contains = false;
1143
896
 
1144
897
  if (window.jQuery) {
@@ -1150,7 +903,12 @@
1150
903
  }
1151
904
 
1152
905
  if (!contains && !$select.clickTriggeredSelect) {
1153
- $select.close(angular.element(e.target).closest('.ui-select-container.open').length > 0); // Skip focusser if the target is another select
906
+ //Will lose focus only with certain targets
907
+ var focusableControls = ['input','button','textarea'];
908
+ var targetScope = angular.element(e.target).scope(); //To check if target is other ui-select
909
+ var skipFocusser = targetScope && targetScope.$select && targetScope.$select !== $select; //To check if target is other ui-select
910
+ if (!skipFocusser) skipFocusser = ~focusableControls.indexOf(e.target.tagName.toLowerCase()); //Check if target is input, button or textarea
911
+ $select.close(skipFocusser);
1154
912
  scope.$digest();
1155
913
  }
1156
914
  $select.clickTriggeredSelect = false;
@@ -1174,6 +932,7 @@
1174
932
 
1175
933
  var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
1176
934
  transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr
935
+ transcludedMatch.removeAttr('data-ui-select-match'); // Properly handle HTML5 data-attributes
1177
936
  if (transcludedMatch.length !== 1) {
1178
937
  throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
1179
938
  }
@@ -1181,147 +940,877 @@
1181
940
 
1182
941
  var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
1183
942
  transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr
943
+ transcludedChoices.removeAttr('data-ui-select-choices'); // Properly handle HTML5 data-attributes
1184
944
  if (transcludedChoices.length !== 1) {
1185
945
  throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
1186
946
  }
1187
947
  element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
1188
948
  });
1189
- }
1190
- };
1191
- }])
1192
949
 
1193
- .directive('uiSelectChoices',
1194
- ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr', '$compile',
1195
- function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile) {
950
+ // Support for appending the select field to the body when its open
951
+ var appendToBody = scope.$eval(attrs.appendToBody);
952
+ if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
953
+ scope.$watch('$select.open', function(isOpen) {
954
+ if (isOpen) {
955
+ positionDropdown();
956
+ } else {
957
+ resetDropdown();
958
+ }
959
+ });
1196
960
 
1197
- return {
1198
- restrict: 'EA',
1199
- require: '^uiSelect',
1200
- replace: true,
1201
- transclude: true,
1202
- templateUrl: function(tElement) {
1203
- // Gets theme attribute from parent (ui-select)
1204
- var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
1205
- return theme + '/choices.tpl.html';
1206
- },
961
+ // Move the dropdown back to its original location when the scope is destroyed. Otherwise
962
+ // it might stick around when the user routes away or the select field is otherwise removed
963
+ scope.$on('$destroy', function() {
964
+ resetDropdown();
965
+ });
966
+ }
1207
967
 
1208
- compile: function(tElement, tAttrs) {
968
+ // Hold on to a reference to the .ui-select-container element for appendToBody support
969
+ var placeholder = null,
970
+ originalWidth = '';
1209
971
 
1210
- if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
972
+ function positionDropdown() {
973
+ // Remember the absolute position of the element
974
+ var offset = uisOffset(element);
1211
975
 
1212
- return function link(scope, element, attrs, $select, transcludeFn) {
976
+ // Clone the element into a placeholder element to take its original place in the DOM
977
+ placeholder = angular.element('<div class="ui-select-placeholder"></div>');
978
+ placeholder[0].style.width = offset.width + 'px';
979
+ placeholder[0].style.height = offset.height + 'px';
980
+ element.after(placeholder);
1213
981
 
1214
- // var repeat = RepeatParser.parse(attrs.repeat);
1215
- var groupByExp = attrs.groupBy;
982
+ // Remember the original value of the element width inline style, so it can be restored
983
+ // when the dropdown is closed
984
+ originalWidth = element[0].style.width;
1216
985
 
1217
- $select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult
986
+ // Now move the actual dropdown element to the end of the body
987
+ $document.find('body').append(element);
1218
988
 
1219
- $select.disableChoiceExpression = attrs.uiDisableChoice;
1220
- $select.onHighlightCallback = attrs.onHighlight;
989
+ element[0].style.position = 'absolute';
990
+ element[0].style.left = offset.left + 'px';
991
+ element[0].style.top = offset.top + 'px';
992
+ element[0].style.width = offset.width + 'px';
993
+ }
1221
994
 
1222
- if(groupByExp) {
1223
- var groups = element.querySelectorAll('.ui-select-choices-group');
1224
- if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length);
1225
- groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
995
+ function resetDropdown() {
996
+ if (placeholder === null) {
997
+ // The dropdown has not actually been display yet, so there's nothing to reset
998
+ return;
1226
999
  }
1227
1000
 
1228
- var choices = element.querySelectorAll('.ui-select-choices-row');
1229
- if (choices.length !== 1) {
1230
- throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
1001
+ // Move the dropdown element back to its original location in the DOM
1002
+ placeholder.replaceWith(element);
1003
+ placeholder = null;
1004
+
1005
+ element[0].style.position = '';
1006
+ element[0].style.left = '';
1007
+ element[0].style.top = '';
1008
+ element[0].style.width = originalWidth;
1009
+ }
1010
+
1011
+ // Hold on to a reference to the .ui-select-dropdown element for direction support.
1012
+ var dropdown = null,
1013
+ directionUpClassName = 'direction-up';
1014
+
1015
+ // Support changing the direction of the dropdown if there isn't enough space to render it.
1016
+ scope.$watch('$select.open', function(isOpen) {
1017
+ if (isOpen) {
1018
+ dropdown = angular.element(element).querySelectorAll('.ui-select-dropdown');
1019
+ if (dropdown === null) {
1020
+ return;
1021
+ }
1022
+
1023
+ // Hide the dropdown so there is no flicker until $timeout is done executing.
1024
+ dropdown[0].style.visibility = 'hidden';
1025
+
1026
+ // Delay positioning the dropdown until all choices have been added so its height is correct.
1027
+ $timeout(function(){
1028
+ var offset = uisOffset(element);
1029
+ var offsetDropdown = uisOffset(dropdown);
1030
+
1031
+ // Determine if the direction of the dropdown needs to be changed.
1032
+ if (offset.top + offset.height + offsetDropdown.height > $document[0].documentElement.scrollTop + $document[0].documentElement.clientHeight) {
1033
+ dropdown[0].style.position = 'absolute';
1034
+ dropdown[0].style.top = (offsetDropdown.height * -1) + 'px';
1035
+ element.addClass(directionUpClassName);
1036
+ }
1037
+
1038
+ // Display the dropdown once it has been positioned.
1039
+ dropdown[0].style.visibility = '';
1040
+ });
1041
+ } else {
1042
+ if (dropdown === null) {
1043
+ return;
1044
+ }
1045
+
1046
+ // Reset the position of the dropdown.
1047
+ dropdown[0].style.position = '';
1048
+ dropdown[0].style.top = '';
1049
+ element.removeClass(directionUpClassName);
1231
1050
  }
1051
+ });
1052
+ };
1053
+ }
1054
+ };
1055
+ }]);
1056
+
1057
+ uis.directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) {
1058
+ return {
1059
+ restrict: 'EA',
1060
+ require: '^uiSelect',
1061
+ replace: true,
1062
+ transclude: true,
1063
+ templateUrl: function(tElement) {
1064
+ // Gets theme attribute from parent (ui-select)
1065
+ var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
1066
+ var multi = tElement.parent().attr('multiple');
1067
+ return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html');
1068
+ },
1069
+ link: function(scope, element, attrs, $select) {
1070
+ $select.lockChoiceExpression = attrs.uiLockChoice;
1071
+ attrs.$observe('placeholder', function(placeholder) {
1072
+ $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
1073
+ });
1232
1074
 
1233
- choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp))
1234
- .attr('ng-if', '$select.open') //Prevent unnecessary watches when dropdown is closed
1235
- .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')')
1236
- .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ',false,$event)');
1075
+ function setAllowClear(allow) {
1076
+ $select.allowClear = (angular.isDefined(allow)) ? (allow === '') ? true : (allow.toLowerCase() === 'true') : false;
1077
+ }
1237
1078
 
1238
- var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner');
1239
- if (rowsInner.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length);
1240
- rowsInner.attr('uis-transclude-append', ''); //Adding uisTranscludeAppend directive to row element after choices element has ngRepeat
1079
+ attrs.$observe('allowClear', setAllowClear);
1080
+ setAllowClear(attrs.allowClear);
1241
1081
 
1242
- $compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend
1082
+ if($select.multiple){
1083
+ $select.sizeSearchInput();
1084
+ }
1243
1085
 
1244
- scope.$watch('$select.search', function(newValue) {
1245
- if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
1246
- $select.activeIndex = $select.tagging.isActivated ? -1 : 0;
1247
- $select.refresh(attrs.refresh);
1248
- });
1086
+ }
1087
+ };
1088
+ }]);
1089
+
1090
+ uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelectMinErr, $timeout) {
1091
+ return {
1092
+ restrict: 'EA',
1093
+ require: ['^uiSelect', '^ngModel'],
1094
+
1095
+ controller: ['$scope','$timeout', function($scope, $timeout){
1096
+
1097
+ var ctrl = this,
1098
+ $select = $scope.$select,
1099
+ ngModel;
1100
+
1101
+ //Wait for link fn to inject it
1102
+ $scope.$evalAsync(function(){ ngModel = $scope.ngModel; });
1103
+
1104
+ ctrl.activeMatchIndex = -1;
1105
+
1106
+ ctrl.updateModel = function(){
1107
+ ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
1108
+ ctrl.refreshComponent();
1109
+ };
1110
+
1111
+ ctrl.refreshComponent = function(){
1112
+ //Remove already selected items
1113
+ //e.g. When user clicks on a selection, the selected array changes and
1114
+ //the dropdown should remove that item
1115
+ $select.refreshItems();
1116
+ $select.sizeSearchInput();
1117
+ };
1118
+
1119
+ // Remove item from multiple select
1120
+ ctrl.removeChoice = function(index){
1121
+
1122
+ var removedChoice = $select.selected[index];
1123
+
1124
+ // if the choice is locked, can't remove it
1125
+ if(removedChoice._uiSelectChoiceLocked) return;
1126
+
1127
+ var locals = {};
1128
+ locals[$select.parserResult.itemName] = removedChoice;
1249
1129
 
1250
- attrs.$observe('refreshDelay', function() {
1251
- // $eval() is needed otherwise we get a string instead of a number
1252
- var refreshDelay = scope.$eval(attrs.refreshDelay);
1253
- $select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay;
1130
+ $select.selected.splice(index, 1);
1131
+ ctrl.activeMatchIndex = -1;
1132
+ $select.sizeSearchInput();
1133
+
1134
+ // Give some time for scope propagation.
1135
+ $timeout(function(){
1136
+ $select.onRemoveCallback($scope, {
1137
+ $item: removedChoice,
1138
+ $model: $select.parserResult.modelMapper($scope, locals)
1254
1139
  });
1140
+ });
1141
+
1142
+ ctrl.updateModel();
1143
+
1144
+ };
1145
+
1146
+ ctrl.getPlaceholder = function(){
1147
+ //Refactor single?
1148
+ if($select.selected.length) return;
1149
+ return $select.placeholder;
1150
+ };
1151
+
1152
+
1153
+ }],
1154
+ controllerAs: '$selectMultiple',
1155
+
1156
+ link: function(scope, element, attrs, ctrls) {
1157
+
1158
+ var $select = ctrls[0];
1159
+ var ngModel = scope.ngModel = ctrls[1];
1160
+ var $selectMultiple = scope.$selectMultiple;
1161
+
1162
+ //$select.selected = raw selected objects (ignoring any property binding)
1163
+
1164
+ $select.multiple = true;
1165
+ $select.removeSelected = true;
1166
+
1167
+ //Input that will handle focus
1168
+ $select.focusInput = $select.searchInput;
1169
+
1170
+ //From view --> model
1171
+ ngModel.$parsers.unshift(function () {
1172
+ var locals = {},
1173
+ result,
1174
+ resultMultiple = [];
1175
+ for (var j = $select.selected.length - 1; j >= 0; j--) {
1176
+ locals = {};
1177
+ locals[$select.parserResult.itemName] = $select.selected[j];
1178
+ result = $select.parserResult.modelMapper(scope, locals);
1179
+ resultMultiple.unshift(result);
1180
+ }
1181
+ return resultMultiple;
1182
+ });
1183
+
1184
+ // From model --> view
1185
+ ngModel.$formatters.unshift(function (inputValue) {
1186
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
1187
+ locals = {},
1188
+ result;
1189
+ if (!data) return inputValue;
1190
+ var resultMultiple = [];
1191
+ var checkFnMultiple = function(list, value){
1192
+ if (!list || !list.length) return;
1193
+ for (var p = list.length - 1; p >= 0; p--) {
1194
+ locals[$select.parserResult.itemName] = list[p];
1195
+ result = $select.parserResult.modelMapper(scope, locals);
1196
+ if($select.parserResult.trackByExp){
1197
+ var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
1198
+ if(matches.length>0 && result[matches[1]] == value[matches[1]]){
1199
+ resultMultiple.unshift(list[p]);
1200
+ return true;
1201
+ }
1202
+ }
1203
+ if (angular.equals(result,value)){
1204
+ resultMultiple.unshift(list[p]);
1205
+ return true;
1206
+ }
1207
+ }
1208
+ return false;
1255
1209
  };
1210
+ if (!inputValue) return resultMultiple; //If ngModel was undefined
1211
+ for (var k = inputValue.length - 1; k >= 0; k--) {
1212
+ //Check model array of currently selected items
1213
+ if (!checkFnMultiple($select.selected, inputValue[k])){
1214
+ //Check model array of all items available
1215
+ if (!checkFnMultiple(data, inputValue[k])){
1216
+ //If not found on previous lists, just add it directly to resultMultiple
1217
+ resultMultiple.unshift(inputValue[k]);
1218
+ }
1219
+ }
1220
+ }
1221
+ return resultMultiple;
1222
+ });
1223
+
1224
+ //Watch for external model changes
1225
+ scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
1226
+ if (oldValue != newValue){
1227
+ ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
1228
+ $selectMultiple.refreshComponent();
1229
+ }
1230
+ });
1231
+
1232
+ ngModel.$render = function() {
1233
+ // Make sure that model value is array
1234
+ if(!angular.isArray(ngModel.$viewValue)){
1235
+ // Have tolerance for null or undefined values
1236
+ if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){
1237
+ $select.selected = [];
1238
+ } else {
1239
+ throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
1240
+ }
1241
+ }
1242
+ $select.selected = ngModel.$viewValue;
1243
+ scope.$evalAsync(); //To force $digest
1244
+ };
1245
+
1246
+ scope.$on('uis:select', function (event, item) {
1247
+ $select.selected.push(item);
1248
+ $selectMultiple.updateModel();
1249
+ });
1250
+
1251
+ scope.$on('uis:activate', function () {
1252
+ $selectMultiple.activeMatchIndex = -1;
1253
+ });
1254
+
1255
+ scope.$watch('$select.disabled', function(newValue, oldValue) {
1256
+ // As the search input field may now become visible, it may be necessary to recompute its size
1257
+ if (oldValue && !newValue) $select.sizeSearchInput();
1258
+ });
1259
+
1260
+ $select.searchInput.on('keydown', function(e) {
1261
+ var key = e.which;
1262
+ scope.$apply(function() {
1263
+ var processed = false;
1264
+ // var tagged = false; //Checkme
1265
+ if(KEY.isHorizontalMovement(key)){
1266
+ processed = _handleMatchSelection(key);
1267
+ }
1268
+ if (processed && key != KEY.TAB) {
1269
+ //TODO Check si el tab selecciona aun correctamente
1270
+ //Crear test
1271
+ e.preventDefault();
1272
+ e.stopPropagation();
1273
+ }
1274
+ });
1275
+ });
1276
+ function _getCaretPosition(el) {
1277
+ if(angular.isNumber(el.selectionStart)) return el.selectionStart;
1278
+ // selectionStart is not supported in IE8 and we don't want hacky workarounds so we compromise
1279
+ else return el.value.length;
1256
1280
  }
1257
- };
1258
- }])
1259
- // Recreates old behavior of ng-transclude. Used internally.
1260
- .directive('uisTranscludeAppend', function () {
1261
- return {
1262
- link: function (scope, element, attrs, ctrl, transclude) {
1263
- transclude(scope, function (clone) {
1264
- element.append(clone);
1281
+ // Handles selected options in "multiple" mode
1282
+ function _handleMatchSelection(key){
1283
+ var caretPosition = _getCaretPosition($select.searchInput[0]),
1284
+ length = $select.selected.length,
1285
+ // none = -1,
1286
+ first = 0,
1287
+ last = length-1,
1288
+ curr = $selectMultiple.activeMatchIndex,
1289
+ next = $selectMultiple.activeMatchIndex+1,
1290
+ prev = $selectMultiple.activeMatchIndex-1,
1291
+ newIndex = curr;
1292
+
1293
+ if(caretPosition > 0 || ($select.search.length && key == KEY.RIGHT)) return false;
1294
+
1295
+ $select.close();
1296
+
1297
+ function getNewActiveMatchIndex(){
1298
+ switch(key){
1299
+ case KEY.LEFT:
1300
+ // Select previous/first item
1301
+ if(~$selectMultiple.activeMatchIndex) return prev;
1302
+ // Select last item
1303
+ else return last;
1304
+ break;
1305
+ case KEY.RIGHT:
1306
+ // Open drop-down
1307
+ if(!~$selectMultiple.activeMatchIndex || curr === last){
1308
+ $select.activate();
1309
+ return false;
1310
+ }
1311
+ // Select next/last item
1312
+ else return next;
1313
+ break;
1314
+ case KEY.BACKSPACE:
1315
+ // Remove selected item and select previous/first
1316
+ if(~$selectMultiple.activeMatchIndex){
1317
+ $selectMultiple.removeChoice(curr);
1318
+ return prev;
1319
+ }
1320
+ // Select last item
1321
+ else return last;
1322
+ break;
1323
+ case KEY.DELETE:
1324
+ // Remove selected item and select next item
1325
+ if(~$selectMultiple.activeMatchIndex){
1326
+ $selectMultiple.removeChoice($selectMultiple.activeMatchIndex);
1327
+ return curr;
1328
+ }
1329
+ else return false;
1330
+ }
1331
+ }
1332
+
1333
+ newIndex = getNewActiveMatchIndex();
1334
+
1335
+ if(!$select.selected.length || newIndex === false) $selectMultiple.activeMatchIndex = -1;
1336
+ else $selectMultiple.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
1337
+
1338
+ return true;
1339
+ }
1340
+
1341
+ $select.searchInput.on('keyup', function(e) {
1342
+
1343
+ if ( ! KEY.isVerticalMovement(e.which) ) {
1344
+ scope.$evalAsync( function () {
1345
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
1265
1346
  });
1266
1347
  }
1348
+ // Push a "create new" item into array if there is a search string
1349
+ if ( $select.tagging.isActivated && $select.search.length > 0 ) {
1350
+
1351
+ // return early with these keys
1352
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
1353
+ return;
1354
+ }
1355
+ // always reset the activeIndex to the first item when tagging
1356
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
1357
+ // taggingLabel === false bypasses all of this
1358
+ if ($select.taggingLabel === false) return;
1359
+
1360
+ var items = angular.copy( $select.items );
1361
+ var stashArr = angular.copy( $select.items );
1362
+ var newItem;
1363
+ var item;
1364
+ var hasTag = false;
1365
+ var dupeIndex = -1;
1366
+ var tagItems;
1367
+ var tagItem;
1368
+
1369
+ // case for object tagging via transform `$select.tagging.fct` function
1370
+ if ( $select.tagging.fct !== undefined) {
1371
+ tagItems = $select.$filter('filter')(items,{'isTag': true});
1372
+ if ( tagItems.length > 0 ) {
1373
+ tagItem = tagItems[0];
1374
+ }
1375
+ // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
1376
+ if ( items.length > 0 && tagItem ) {
1377
+ hasTag = true;
1378
+ items = items.slice(1,items.length);
1379
+ stashArr = stashArr.slice(1,stashArr.length);
1380
+ }
1381
+ newItem = $select.tagging.fct($select.search);
1382
+ newItem.isTag = true;
1383
+ // verify the the tag doesn't match the value of an existing item
1384
+ if ( stashArr.filter( function (origItem) { return angular.equals( origItem, $select.tagging.fct($select.search) ); } ).length > 0 ) {
1385
+ return;
1386
+ }
1387
+ newItem.isTag = true;
1388
+ // handle newItem string and stripping dupes in tagging string context
1389
+ } else {
1390
+ // find any tagging items already in the $select.items array and store them
1391
+ tagItems = $select.$filter('filter')(items,function (item) {
1392
+ return item.match($select.taggingLabel);
1393
+ });
1394
+ if ( tagItems.length > 0 ) {
1395
+ tagItem = tagItems[0];
1396
+ }
1397
+ item = items[0];
1398
+ // remove existing tag item if found (should only ever be one tag item)
1399
+ if ( item !== undefined && items.length > 0 && tagItem ) {
1400
+ hasTag = true;
1401
+ items = items.slice(1,items.length);
1402
+ stashArr = stashArr.slice(1,stashArr.length);
1403
+ }
1404
+ newItem = $select.search+' '+$select.taggingLabel;
1405
+ if ( _findApproxDupe($select.selected, $select.search) > -1 ) {
1406
+ return;
1407
+ }
1408
+ // verify the the tag doesn't match the value of an existing item from
1409
+ // the searched data set or the items already selected
1410
+ if ( _findCaseInsensitiveDupe(stashArr.concat($select.selected)) ) {
1411
+ // if there is a tag from prev iteration, strip it / queue the change
1412
+ // and return early
1413
+ if ( hasTag ) {
1414
+ items = stashArr;
1415
+ scope.$evalAsync( function () {
1416
+ $select.activeIndex = 0;
1417
+ $select.items = items;
1418
+ });
1419
+ }
1420
+ return;
1421
+ }
1422
+ if ( _findCaseInsensitiveDupe(stashArr) ) {
1423
+ // if there is a tag from prev iteration, strip it
1424
+ if ( hasTag ) {
1425
+ $select.items = stashArr.slice(1,stashArr.length);
1426
+ }
1427
+ return;
1428
+ }
1429
+ }
1430
+ if ( hasTag ) dupeIndex = _findApproxDupe($select.selected, newItem);
1431
+ // dupe found, shave the first item
1432
+ if ( dupeIndex > -1 ) {
1433
+ items = items.slice(dupeIndex+1,items.length-1);
1434
+ } else {
1435
+ items = [];
1436
+ items.push(newItem);
1437
+ items = items.concat(stashArr);
1438
+ }
1439
+ scope.$evalAsync( function () {
1440
+ $select.activeIndex = 0;
1441
+ $select.items = items;
1442
+ });
1443
+ }
1444
+ });
1445
+ function _findCaseInsensitiveDupe(arr) {
1446
+ if ( arr === undefined || $select.search === undefined ) {
1447
+ return false;
1448
+ }
1449
+ var hasDupe = arr.filter( function (origItem) {
1450
+ if ( $select.search.toUpperCase() === undefined || origItem === undefined ) {
1451
+ return false;
1452
+ }
1453
+ return origItem.toUpperCase() === $select.search.toUpperCase();
1454
+ }).length > 0;
1455
+
1456
+ return hasDupe;
1457
+ }
1458
+ function _findApproxDupe(haystack, needle) {
1459
+ var dupeIndex = -1;
1460
+ if(angular.isArray(haystack)) {
1461
+ var tempArr = angular.copy(haystack);
1462
+ for (var i = 0; i <tempArr.length; i++) {
1463
+ // handle the simple string version of tagging
1464
+ if ( $select.tagging.fct === undefined ) {
1465
+ // search the array for the match
1466
+ if ( tempArr[i]+' '+$select.taggingLabel === needle ) {
1467
+ dupeIndex = i;
1468
+ }
1469
+ // handle the object tagging implementation
1470
+ } else {
1471
+ var mockObj = tempArr[i];
1472
+ mockObj.isTag = true;
1473
+ if ( angular.equals(mockObj, needle) ) {
1474
+ dupeIndex = i;
1475
+ }
1476
+ }
1477
+ }
1478
+ }
1479
+ return dupeIndex;
1480
+ }
1481
+
1482
+ $select.searchInput.on('blur', function() {
1483
+ $timeout(function() {
1484
+ $selectMultiple.activeMatchIndex = -1;
1485
+ });
1486
+ });
1487
+
1488
+ }
1489
+ };
1490
+ }]);
1491
+ uis.directive('uiSelectSingle', ['$timeout','$compile', function($timeout, $compile) {
1492
+ return {
1493
+ restrict: 'EA',
1494
+ require: ['^uiSelect', '^ngModel'],
1495
+ link: function(scope, element, attrs, ctrls) {
1496
+
1497
+ var $select = ctrls[0];
1498
+ var ngModel = ctrls[1];
1499
+
1500
+ //From view --> model
1501
+ ngModel.$parsers.unshift(function (inputValue) {
1502
+ var locals = {},
1503
+ result;
1504
+ locals[$select.parserResult.itemName] = inputValue;
1505
+ result = $select.parserResult.modelMapper(scope, locals);
1506
+ return result;
1507
+ });
1508
+
1509
+ //From model --> view
1510
+ ngModel.$formatters.unshift(function (inputValue) {
1511
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
1512
+ locals = {},
1513
+ result;
1514
+ if (data){
1515
+ var checkFnSingle = function(d){
1516
+ locals[$select.parserResult.itemName] = d;
1517
+ result = $select.parserResult.modelMapper(scope, locals);
1518
+ return result == inputValue;
1519
+ };
1520
+ //If possible pass same object stored in $select.selected
1521
+ if ($select.selected && checkFnSingle($select.selected)) {
1522
+ return $select.selected;
1523
+ }
1524
+ for (var i = data.length - 1; i >= 0; i--) {
1525
+ if (checkFnSingle(data[i])) return data[i];
1526
+ }
1527
+ }
1528
+ return inputValue;
1529
+ });
1530
+
1531
+ //Update viewValue if model change
1532
+ scope.$watch('$select.selected', function(newValue) {
1533
+ if (ngModel.$viewValue !== newValue) {
1534
+ ngModel.$setViewValue(newValue);
1535
+ }
1536
+ });
1537
+
1538
+ ngModel.$render = function() {
1539
+ $select.selected = ngModel.$viewValue;
1267
1540
  };
1268
- })
1269
- .directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) {
1270
- return {
1271
- restrict: 'EA',
1272
- require: '^uiSelect',
1273
- replace: true,
1274
- transclude: true,
1275
- templateUrl: function(tElement) {
1276
- // Gets theme attribute from parent (ui-select)
1277
- var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
1278
- var multi = tElement.parent().attr('multiple');
1279
- return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html');
1280
- },
1281
- link: function(scope, element, attrs, $select) {
1282
- $select.lockChoiceExpression = attrs.uiLockChoice;
1283
- attrs.$observe('placeholder', function(placeholder) {
1284
- $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
1541
+
1542
+ scope.$on('uis:select', function (event, item) {
1543
+ $select.selected = item;
1544
+ });
1545
+
1546
+ scope.$on('uis:close', function (event, skipFocusser) {
1547
+ $timeout(function(){
1548
+ $select.focusser.prop('disabled', false);
1549
+ if (!skipFocusser) $select.focusser[0].focus();
1550
+ },0,false);
1551
+ });
1552
+
1553
+ scope.$on('uis:activate', function () {
1554
+ focusser.prop('disabled', true); //Will reactivate it on .close()
1555
+ });
1556
+
1557
+ //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
1558
+ var focusser = angular.element("<input ng-disabled='$select.disabled' class='ui-select-focusser ui-select-offscreen' type='text' id='{{ $select.focusserId }}' aria-label='{{ $select.focusserTitle }}' aria-haspopup='true' role='button' />");
1559
+ $compile(focusser)(scope);
1560
+ $select.focusser = focusser;
1561
+
1562
+ //Input that will handle focus
1563
+ $select.focusInput = focusser;
1564
+
1565
+ element.parent().append(focusser);
1566
+ focusser.bind("focus", function(){
1567
+ scope.$evalAsync(function(){
1568
+ $select.focus = true;
1285
1569
  });
1570
+ });
1571
+ focusser.bind("blur", function(){
1572
+ scope.$evalAsync(function(){
1573
+ $select.focus = false;
1574
+ });
1575
+ });
1576
+ focusser.bind("keydown", function(e){
1577
+
1578
+ if (e.which === KEY.BACKSPACE) {
1579
+ e.preventDefault();
1580
+ e.stopPropagation();
1581
+ $select.select(undefined);
1582
+ scope.$apply();
1583
+ return;
1584
+ }
1286
1585
 
1287
- $select.allowClear = (angular.isDefined(attrs.allowClear)) ? (attrs.allowClear === '') ? true : (attrs.allowClear.toLowerCase() === 'true') : false;
1586
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1587
+ return;
1588
+ }
1288
1589
 
1289
- if($select.multiple){
1290
- $select.sizeSearchInput();
1590
+ if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){
1591
+ e.preventDefault();
1592
+ e.stopPropagation();
1593
+ $select.activate();
1291
1594
  }
1292
1595
 
1596
+ scope.$digest();
1597
+ });
1598
+
1599
+ focusser.bind("keyup input", function(e){
1600
+
1601
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
1602
+ return;
1603
+ }
1604
+
1605
+ $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
1606
+ focusser.val('');
1607
+ scope.$digest();
1608
+
1609
+ });
1610
+
1611
+
1612
+ }
1613
+ };
1614
+ }]);
1615
+ // Make multiple matches sortable
1616
+ uis.directive('uiSelectSort', ['$timeout', 'uiSelectConfig', 'uiSelectMinErr', function($timeout, uiSelectConfig, uiSelectMinErr) {
1617
+ return {
1618
+ require: '^uiSelect',
1619
+ link: function(scope, element, attrs, $select) {
1620
+ if (scope[attrs.uiSelectSort] === null) {
1621
+ throw uiSelectMinErr('sort', "Expected a list to sort");
1293
1622
  }
1294
- };
1295
- }])
1623
+
1624
+ var options = angular.extend({
1625
+ axis: 'horizontal'
1626
+ },
1627
+ scope.$eval(attrs.uiSelectSortOptions));
1628
+
1629
+ var axis = options.axis,
1630
+ draggingClassName = 'dragging',
1631
+ droppingClassName = 'dropping',
1632
+ droppingBeforeClassName = 'dropping-before',
1633
+ droppingAfterClassName = 'dropping-after';
1634
+
1635
+ scope.$watch(function(){
1636
+ return $select.sortable;
1637
+ }, function(n){
1638
+ if (n) {
1639
+ element.attr('draggable', true);
1640
+ } else {
1641
+ element.removeAttr('draggable');
1642
+ }
1643
+ });
1644
+
1645
+ element.on('dragstart', function(e) {
1646
+ element.addClass(draggingClassName);
1647
+
1648
+ (e.dataTransfer || e.originalEvent.dataTransfer).setData('text/plain', scope.$index);
1649
+ });
1650
+
1651
+ element.on('dragend', function() {
1652
+ element.removeClass(draggingClassName);
1653
+ });
1654
+
1655
+ var move = function(from, to) {
1656
+ /*jshint validthis: true */
1657
+ this.splice(to, 0, this.splice(from, 1)[0]);
1658
+ };
1659
+
1660
+ var dragOverHandler = function(e) {
1661
+ e.preventDefault();
1662
+
1663
+ var offset = axis === 'vertical' ? e.offsetY || e.layerY || (e.originalEvent ? e.originalEvent.offsetY : 0) : e.offsetX || e.layerX || (e.originalEvent ? e.originalEvent.offsetX : 0);
1664
+
1665
+ if (offset < (this[axis === 'vertical' ? 'offsetHeight' : 'offsetWidth'] / 2)) {
1666
+ element.removeClass(droppingAfterClassName);
1667
+ element.addClass(droppingBeforeClassName);
1668
+
1669
+ } else {
1670
+ element.removeClass(droppingBeforeClassName);
1671
+ element.addClass(droppingAfterClassName);
1672
+ }
1673
+ };
1674
+
1675
+ var dropTimeout;
1676
+
1677
+ var dropHandler = function(e) {
1678
+ e.preventDefault();
1679
+
1680
+ var droppedItemIndex = parseInt((e.dataTransfer || e.originalEvent.dataTransfer).getData('text/plain'), 10);
1681
+
1682
+ // prevent event firing multiple times in firefox
1683
+ $timeout.cancel(dropTimeout);
1684
+ dropTimeout = $timeout(function() {
1685
+ _dropHandler(droppedItemIndex);
1686
+ }, 20);
1687
+ };
1688
+
1689
+ var _dropHandler = function(droppedItemIndex) {
1690
+ var theList = scope.$eval(attrs.uiSelectSort),
1691
+ itemToMove = theList[droppedItemIndex],
1692
+ newIndex = null;
1693
+
1694
+ if (element.hasClass(droppingBeforeClassName)) {
1695
+ if (droppedItemIndex < scope.$index) {
1696
+ newIndex = scope.$index - 1;
1697
+ } else {
1698
+ newIndex = scope.$index;
1699
+ }
1700
+ } else {
1701
+ if (droppedItemIndex < scope.$index) {
1702
+ newIndex = scope.$index;
1703
+ } else {
1704
+ newIndex = scope.$index + 1;
1705
+ }
1706
+ }
1707
+
1708
+ move.apply(theList, [droppedItemIndex, newIndex]);
1709
+
1710
+ scope.$apply(function() {
1711
+ scope.$emit('uiSelectSort:change', {
1712
+ array: theList,
1713
+ item: itemToMove,
1714
+ from: droppedItemIndex,
1715
+ to: newIndex
1716
+ });
1717
+ });
1718
+
1719
+ element.removeClass(droppingClassName);
1720
+ element.removeClass(droppingBeforeClassName);
1721
+ element.removeClass(droppingAfterClassName);
1722
+
1723
+ element.off('drop', dropHandler);
1724
+ };
1725
+
1726
+ element.on('dragenter', function() {
1727
+ if (element.hasClass(draggingClassName)) {
1728
+ return;
1729
+ }
1730
+
1731
+ element.addClass(droppingClassName);
1732
+
1733
+ element.on('dragover', dragOverHandler);
1734
+ element.on('drop', dropHandler);
1735
+ });
1736
+
1737
+ element.on('dragleave', function(e) {
1738
+ if (e.target != element) {
1739
+ return;
1740
+ }
1741
+ element.removeClass(droppingClassName);
1742
+ element.removeClass(droppingBeforeClassName);
1743
+ element.removeClass(droppingAfterClassName);
1744
+
1745
+ element.off('dragover', dragOverHandler);
1746
+ element.off('drop', dropHandler);
1747
+ });
1748
+ }
1749
+ };
1750
+ }]);
1751
+
1752
+ /**
1753
+ * Parses "repeat" attribute.
1754
+ *
1755
+ * Taken from AngularJS ngRepeat source code
1756
+ * See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211
1757
+ *
1758
+ * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
1759
+ * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
1760
+ */
1761
+
1762
+ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
1763
+ var self = this;
1296
1764
 
1297
1765
  /**
1298
- * Highlights text that matches $select.search.
1299
- *
1300
- * Taken from AngularUI Bootstrap Typeahead
1301
- * See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340
1766
+ * Example:
1767
+ * expression = "address in addresses | filter: {street: $select.search} track by $index"
1768
+ * itemName = "address",
1769
+ * source = "addresses | filter: {street: $select.search}",
1770
+ * trackByExp = "$index",
1302
1771
  */
1303
- .filter('highlight', function() {
1304
- function escapeRegexp(queryToEscape) {
1305
- return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
1772
+ self.parse = function(expression) {
1773
+
1774
+ var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
1775
+
1776
+ if (!match) {
1777
+ throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
1778
+ expression);
1306
1779
  }
1307
1780
 
1308
- return function(matchItem, query) {
1309
- return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
1781
+ return {
1782
+ itemName: match[2], // (lhs) Left-hand side,
1783
+ source: $parse(match[3]),
1784
+ trackByExp: match[4],
1785
+ modelMapper: $parse(match[1] || match[2])
1310
1786
  };
1311
- });
1312
- }());
1313
1787
 
1788
+ };
1314
1789
 
1315
- angular.module("ui.select").run(["$templateCache", function($templateCache) {$templateCache.put("bootstrap/choices.tpl.html","<ul class=\"ui-select-choices ui-select-choices-content dropdown-menu\" role=\"menu\" aria-labelledby=\"dLabel\" ng-show=\"$select.items.length > 0\"><li class=\"ui-select-choices-group\"><div class=\"divider\" ng-show=\"$select.isGrouped && $index > 0\"></div><div ng-show=\"$select.isGrouped\" class=\"ui-select-choices-group-label dropdown-header\" ng-bind-html=\"$group.name\"></div><div class=\"ui-select-choices-row\" ng-class=\"{active: $select.isActive(this), disabled: $select.isDisabled(this)}\"><a href=\"javascript:void(0)\" class=\"ui-select-choices-row-inner\"></a></div></li></ul>");
1316
- $templateCache.put("bootstrap/match-multiple.tpl.html","<span class=\"ui-select-match\"><span ng-repeat=\"$item in $select.selected\"><span style=\"margin-right: 3px;\" class=\"ui-select-match-item btn btn-default btn-xs\" tabindex=\"-1\" type=\"button\" ng-disabled=\"$select.disabled\" ng-click=\"$select.activeMatchIndex = $index;\" ng-class=\"{\'btn-primary\':$select.activeMatchIndex === $index, \'select-locked\':$select.isLocked(this, $index)}\"><span class=\"close ui-select-match-close\" ng-hide=\"$select.disabled\" ng-click=\"$select.removeChoice($index)\">&nbsp;&times;</span> <span uis-transclude-append=\"\"></span></span></span></span>");
1317
- $templateCache.put("bootstrap/match.tpl.html","<div class=\"btn-group ui-select-match btn-block\" ng-hide=\"$select.open\" ng-disabled=\"$select.disabled\" ng-class=\"{\'btn-default-focus\':$select.focus}\"><button type=\"button\" class=\"btn btn-default\" ng-class=\"{\'col-sm-8 col-md-10\': $select.allowClear && !$select.isEmpty(),\'col-sm-10 col-md-11\': !$select.allowClear || $select.isEmpty()}\" tabindex=\"-1\" ;=\"\" ng-click=\"$select.activate()\"><span ng-show=\"$select.isEmpty()\" class=\"text-muted\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" ng-transclude=\"\"></span></button> <button class=\"btn btn-default col-sm-2 col-md-1\" ng-if=\"$select.allowClear && !$select.isEmpty()\" ng-click=\"$select.select(undefined)\"><span class=\"glyphicon glyphicon-remove ui-select-toggle\"></span></button> <button class=\"btn btn-default col-sm-2 col-md-1\" ng-click=\"$select.activate()\"><span class=\"caret ui-select-toggle\" ng-click=\"$select.toggle($event)\"></span></button></div>");
1318
- $templateCache.put("bootstrap/select-multiple.tpl.html","<div class=\"ui-select-container ui-select-multiple ui-select-bootstrap dropdown form-control\" ng-class=\"{open: $select.open}\"><div><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" class=\"ui-select-search input-xs\" placeholder=\"{{$select.getPlaceholder()}}\" ng-disabled=\"$select.disabled\" ng-hide=\"$select.disabled\" ng-click=\"$select.activate()\" ng-model=\"$select.search\"></div><div class=\"ui-select-choices\"></div></div>");
1319
- $templateCache.put("bootstrap/select.tpl.html","<div class=\"ui-select-container ui-select-bootstrap dropdown\" ng-class=\"{open: $select.open}\"><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" tabindex=\"-1\" class=\"form-control ui-select-search\" placeholder=\"{{$select.placeholder}}\" ng-model=\"$select.search\" ng-show=\"$select.searchEnabled && $select.open\"><div class=\"ui-select-choices\"></div></div>");
1320
- $templateCache.put("select2/choices.tpl.html","<ul class=\"ui-select-choices ui-select-choices-content select2-results\"><li class=\"ui-select-choices-group\" ng-class=\"{\'select2-result-with-children\': $select.choiceGrouped($group) }\"><div ng-show=\"$select.choiceGrouped($group)\" class=\"ui-select-choices-group-label select2-result-label\" ng-bind-html=\"$group.name\"></div><ul ng-class=\"{\'select2-result-sub\': $select.choiceGrouped($group), \'select2-result-single\': !$select.choiceGrouped($group) }\"><li class=\"ui-select-choices-row\" ng-class=\"{\'select2-highlighted\': $select.isActive(this), \'select2-disabled\': $select.isDisabled(this)}\"><div class=\"select2-result-label ui-select-choices-row-inner\"></div></li></ul></li></ul>");
1321
- $templateCache.put("select2/match-multiple.tpl.html","<span class=\"ui-select-match\"><li class=\"ui-select-match-item select2-search-choice\" ng-repeat=\"$item in $select.selected\" ng-class=\"{\'select2-search-choice-focus\':$select.activeMatchIndex === $index, \'select2-locked\':$select.isLocked(this, $index)}\"><span uis-transclude-append=\"\"></span> <a href=\"javascript:;\" class=\"ui-select-match-close select2-search-choice-close\" ng-click=\"$select.removeChoice($index)\" tabindex=\"-1\"></a></li></span>");
1322
- $templateCache.put("select2/match.tpl.html","<a class=\"select2-choice ui-select-match\" ng-class=\"{\'select2-default\': $select.isEmpty()}\" ng-click=\"$select.activate()\"><span ng-show=\"$select.isEmpty()\" class=\"select2-chosen\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"select2-chosen\" ng-transclude=\"\"></span> <abbr ng-if=\"$select.allowClear && !$select.isEmpty()\" class=\"select2-search-choice-close\" ng-click=\"$select.select(undefined)\"></abbr> <span class=\"select2-arrow ui-select-toggle\" ng-click=\"$select.toggle($event)\"><b></b></span></a>");
1323
- $templateCache.put("select2/select-multiple.tpl.html","<div class=\"ui-select-container ui-select-multiple select2 select2-container select2-container-multi\" ng-class=\"{\'select2-container-active select2-dropdown-open open\': $select.open,\n \'select2-container-disabled\': $select.disabled}\"><ul class=\"select2-choices\"><span class=\"ui-select-match\"></span><li class=\"select2-search-field\"><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" class=\"select2-input ui-select-search\" placeholder=\"{{$select.getPlaceholder()}}\" ng-disabled=\"$select.disabled\" ng-hide=\"$select.disabled\" ng-model=\"$select.search\" ng-click=\"$select.activate()\" style=\"width: 34px;\"></li></ul><div class=\"select2-drop select2-with-searchbox select2-drop-active\" ng-class=\"{\'select2-display-none\': !$select.open}\"><div class=\"ui-select-choices\"></div></div></div>");
1324
- $templateCache.put("select2/select.tpl.html","<div class=\"ui-select-container select2 select2-container\" ng-class=\"{\'select2-container-active select2-dropdown-open open\': $select.open,\n \'select2-container-disabled\': $select.disabled,\n \'select2-container-active\': $select.focus, \n \'select2-allowclear\': $select.allowClear && !$select.isEmpty()}\"><div class=\"ui-select-match\"></div><div class=\"select2-drop select2-with-searchbox select2-drop-active\" ng-class=\"{\'select2-display-none\': !$select.open}\"><div class=\"select2-search\" ng-show=\"$select.searchEnabled\"><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" class=\"ui-select-search select2-input\" ng-model=\"$select.search\"></div><div class=\"ui-select-choices\"></div></div></div>");
1325
- $templateCache.put("selectize/choices.tpl.html","<div ng-show=\"$select.open\" class=\"ui-select-choices selectize-dropdown single\"><div class=\"ui-select-choices-content selectize-dropdown-content\"><div class=\"ui-select-choices-group optgroup\"><div ng-show=\"$select.isGrouped\" class=\"ui-select-choices-group-label optgroup-header\" ng-bind-html=\"$group.name\"></div><div class=\"ui-select-choices-row\" ng-class=\"{active: $select.isActive(this), disabled: $select.isDisabled(this)}\"><div class=\"option ui-select-choices-row-inner\" data-selectable=\"\"></div></div></div></div></div>");
1790
+ self.getGroupNgRepeatExpression = function() {
1791
+ return '$group in $select.groups';
1792
+ };
1793
+
1794
+ self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) {
1795
+ var expression = itemName + ' in ' + (grouped ? '$group.items' : source);
1796
+ if (trackByExp) {
1797
+ expression += ' track by ' + trackByExp;
1798
+ }
1799
+ return expression;
1800
+ };
1801
+ }]);
1802
+
1803
+ }());
1804
+ angular.module("ui.select").run(["$templateCache", function($templateCache) {$templateCache.put("bootstrap/choices.tpl.html","<ul class=\"ui-select-choices ui-select-choices-content ui-select-dropdown dropdown-menu\" role=\"listbox\" ng-show=\"$select.items.length > 0\"><li class=\"ui-select-choices-group\" id=\"ui-select-choices-{{ $select.generatedId }}\"><div class=\"divider\" ng-show=\"$select.isGrouped && $index > 0\"></div><div ng-show=\"$select.isGrouped\" class=\"ui-select-choices-group-label dropdown-header\" ng-bind=\"$group.name\"></div><div id=\"ui-select-choices-row-{{ $select.generatedId }}-{{$index}}\" class=\"ui-select-choices-row\" ng-class=\"{active: $select.isActive(this), disabled: $select.isDisabled(this)}\" role=\"option\"><a href=\"javascript:void(0)\" class=\"ui-select-choices-row-inner\"></a></div></li></ul>");
1805
+ $templateCache.put("bootstrap/match-multiple.tpl.html","<span class=\"ui-select-match\"><span ng-repeat=\"$item in $select.selected\"><span class=\"ui-select-match-item btn btn-default btn-xs\" tabindex=\"-1\" type=\"button\" ng-disabled=\"$select.disabled\" ng-click=\"$selectMultiple.activeMatchIndex = $index;\" ng-class=\"{\'btn-primary\':$selectMultiple.activeMatchIndex === $index, \'select-locked\':$select.isLocked(this, $index)}\" ui-select-sort=\"$select.selected\"><span class=\"close ui-select-match-close\" ng-hide=\"$select.disabled\" ng-click=\"$selectMultiple.removeChoice($index)\">&nbsp;&times;</span> <span uis-transclude-append=\"\"></span></span></span></span>");
1806
+ $templateCache.put("bootstrap/match.tpl.html","<div class=\"ui-select-match\" ng-hide=\"$select.open\" ng-disabled=\"$select.disabled\" ng-class=\"{\'btn-default-focus\':$select.focus}\"><span tabindex=\"-1\" class=\"btn btn-default form-control ui-select-toggle\" aria-label=\"{{ $select.baseTitle }} activate\" ng-disabled=\"$select.disabled\" ng-click=\"$select.activate()\" style=\"outline: 0;\"><span ng-show=\"$select.isEmpty()\" class=\"ui-select-placeholder text-muted\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"ui-select-match-text pull-left\" ng-class=\"{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}\" ng-transclude=\"\"></span> <i class=\"caret pull-right\" ng-click=\"$select.toggle($event)\"></i> <a ng-show=\"$select.allowClear && !$select.isEmpty()\" aria-label=\"{{ $select.baseTitle }} clear\" style=\"margin-right: 10px\" ng-click=\"$select.clear($event)\" class=\"btn btn-xs btn-link pull-right\"><i class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></i></a></span></div>");
1807
+ $templateCache.put("bootstrap/select-multiple.tpl.html","<div class=\"ui-select-container ui-select-multiple ui-select-bootstrap dropdown form-control\" ng-class=\"{open: $select.open}\"><div><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" class=\"ui-select-search input-xs\" placeholder=\"{{$selectMultiple.getPlaceholder()}}\" ng-disabled=\"$select.disabled\" ng-hide=\"$select.disabled\" ng-click=\"$select.activate()\" ng-model=\"$select.search\" role=\"combobox\" aria-label=\"{{ $select.baseTitle }}\" ondrop=\"return false;\"></div><div class=\"ui-select-choices\"></div></div>");
1808
+ $templateCache.put("bootstrap/select.tpl.html","<div class=\"ui-select-container ui-select-bootstrap dropdown\" ng-class=\"{open: $select.open}\"><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" tabindex=\"-1\" aria-expanded=\"true\" aria-label=\"{{ $select.baseTitle }}\" aria-owns=\"ui-select-choices-{{ $select.generatedId }}\" aria-activedescendant=\"ui-select-choices-row-{{ $select.generatedId }}-{{ $select.activeIndex }}\" class=\"form-control ui-select-search\" placeholder=\"{{$select.placeholder}}\" ng-model=\"$select.search\" ng-show=\"$select.searchEnabled && $select.open\"><div class=\"ui-select-choices\"></div></div>");
1809
+ $templateCache.put("select2/choices.tpl.html","<ul class=\"ui-select-choices ui-select-choices-content select2-results\"><li class=\"ui-select-choices-group\" ng-class=\"{\'select2-result-with-children\': $select.choiceGrouped($group) }\"><div ng-show=\"$select.choiceGrouped($group)\" class=\"ui-select-choices-group-label select2-result-label\" ng-bind=\"$group.name\"></div><ul role=\"listbox\" id=\"ui-select-choices-{{ $select.generatedId }}\" ng-class=\"{\'select2-result-sub\': $select.choiceGrouped($group), \'select2-result-single\': !$select.choiceGrouped($group) }\"><li role=\"option\" id=\"ui-select-choices-row-{{ $select.generatedId }}-{{$index}}\" class=\"ui-select-choices-row\" ng-class=\"{\'select2-highlighted\': $select.isActive(this), \'select2-disabled\': $select.isDisabled(this)}\"><div class=\"select2-result-label ui-select-choices-row-inner\"></div></li></ul></li></ul>");
1810
+ $templateCache.put("select2/match-multiple.tpl.html","<span class=\"ui-select-match\"><li class=\"ui-select-match-item select2-search-choice\" ng-repeat=\"$item in $select.selected\" ng-class=\"{\'select2-search-choice-focus\':$selectMultiple.activeMatchIndex === $index, \'select2-locked\':$select.isLocked(this, $index)}\" ui-select-sort=\"$select.selected\"><span uis-transclude-append=\"\"></span> <a href=\"javascript:;\" class=\"ui-select-match-close select2-search-choice-close\" ng-click=\"$selectMultiple.removeChoice($index)\" tabindex=\"-1\"></a></li></span>");
1811
+ $templateCache.put("select2/match.tpl.html","<a class=\"select2-choice ui-select-match\" ng-class=\"{\'select2-default\': $select.isEmpty()}\" ng-click=\"$select.toggle($event)\" aria-label=\"{{ $select.baseTitle }} select\"><span ng-show=\"$select.isEmpty()\" class=\"select2-chosen\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"select2-chosen\" ng-transclude=\"\"></span> <abbr ng-if=\"$select.allowClear && !$select.isEmpty()\" class=\"select2-search-choice-close\" ng-click=\"$select.clear($event)\"></abbr> <span class=\"select2-arrow ui-select-toggle\"><b></b></span></a>");
1812
+ $templateCache.put("select2/select-multiple.tpl.html","<div class=\"ui-select-container ui-select-multiple select2 select2-container select2-container-multi\" ng-class=\"{\'select2-container-active select2-dropdown-open open\': $select.open, \'select2-container-disabled\': $select.disabled}\"><ul class=\"select2-choices\"><span class=\"ui-select-match\"></span><li class=\"select2-search-field\"><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" role=\"combobox\" aria-expanded=\"true\" aria-owns=\"ui-select-choices-{{ $select.generatedId }}\" aria-label=\"{{ $select.baseTitle }}\" aria-activedescendant=\"ui-select-choices-row-{{ $select.generatedId }}-{{ $select.activeIndex }}\" class=\"select2-input ui-select-search\" placeholder=\"{{$selectMultiple.getPlaceholder()}}\" ng-disabled=\"$select.disabled\" ng-hide=\"$select.disabled\" ng-model=\"$select.search\" ng-click=\"$select.activate()\" style=\"width: 34px;\" ondrop=\"return false;\"></li></ul><div class=\"ui-select-dropdown select2-drop select2-with-searchbox select2-drop-active\" ng-class=\"{\'select2-display-none\': !$select.open}\"><div class=\"ui-select-choices\"></div></div></div>");
1813
+ $templateCache.put("select2/select.tpl.html","<div class=\"ui-select-container select2 select2-container\" ng-class=\"{\'select2-container-active select2-dropdown-open open\': $select.open, \'select2-container-disabled\': $select.disabled, \'select2-container-active\': $select.focus, \'select2-allowclear\': $select.allowClear && !$select.isEmpty()}\"><div class=\"ui-select-match\"></div><div class=\"ui-select-dropdown select2-drop select2-with-searchbox select2-drop-active\" ng-class=\"{\'select2-display-none\': !$select.open}\"><div class=\"select2-search\" ng-show=\"$select.searchEnabled\"><input type=\"text\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\" role=\"combobox\" aria-expanded=\"true\" aria-owns=\"ui-select-choices-{{ $select.generatedId }}\" aria-label=\"{{ $select.baseTitle }}\" aria-activedescendant=\"ui-select-choices-row-{{ $select.generatedId }}-{{ $select.activeIndex }}\" class=\"ui-select-search select2-input\" ng-model=\"$select.search\"></div><div class=\"ui-select-choices\"></div></div></div>");
1814
+ $templateCache.put("selectize/choices.tpl.html","<div ng-show=\"$select.open\" class=\"ui-select-choices ui-select-dropdown selectize-dropdown single\"><div class=\"ui-select-choices-content selectize-dropdown-content\"><div class=\"ui-select-choices-group optgroup\" role=\"listbox\"><div ng-show=\"$select.isGrouped\" class=\"ui-select-choices-group-label optgroup-header\" ng-bind=\"$group.name\"></div><div role=\"option\" class=\"ui-select-choices-row\" ng-class=\"{active: $select.isActive(this), disabled: $select.isDisabled(this)}\"><div class=\"option ui-select-choices-row-inner\" data-selectable=\"\"></div></div></div></div></div>");
1326
1815
  $templateCache.put("selectize/match.tpl.html","<div ng-hide=\"($select.open || $select.isEmpty())\" class=\"ui-select-match\" ng-transclude=\"\"></div>");
1327
- $templateCache.put("selectize/select.tpl.html","<div class=\"ui-select-container selectize-control single\" ng-class=\"{\'open\': $select.open}\"><div class=\"selectize-input\" ng-class=\"{\'focus\': $select.open, \'disabled\': $select.disabled, \'selectize-focus\' : $select.focus}\" ng-click=\"$select.activate()\"><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" tabindex=\"-1\" class=\"ui-select-search ui-select-toggle\" ng-click=\"$select.toggle($event)\" placeholder=\"{{$select.placeholder}}\" ng-model=\"$select.search\" ng-hide=\"!$select.searchEnabled || ($select.selected && !$select.open)\" ng-disabled=\"$select.disabled\"></div><div class=\"ui-select-choices\"></div></div>");}]);
1816
+ $templateCache.put("selectize/select.tpl.html","<div class=\"ui-select-container selectize-control single\" ng-class=\"{\'open\': $select.open}\"><div class=\"selectize-input\" ng-class=\"{\'focus\': $select.open, \'disabled\': $select.disabled, \'selectize-focus\' : $select.focus}\" ng-click=\"$select.activate()\"><div class=\"ui-select-match\"></div><input type=\"text\" autocomplete=\"off\" tabindex=\"-1\" class=\"ui-select-search ui-select-toggle\" ng-click=\"$select.toggle($event)\" placeholder=\"{{$select.placeholder}}\" ng-model=\"$select.search\" ng-hide=\"!$select.searchEnabled || ($select.selected && !$select.open)\" ng-disabled=\"$select.disabled\" aria-label=\"{{ $select.baseTitle }}\"></div><div class=\"ui-select-choices\"></div></div>");}]);