diff --git a/src/select2/select-multiple.tpl.html b/src/select2/select-multiple.tpl.html
index 6d8ed0d19..ca65f455b 100644
--- a/src/select2/select-multiple.tpl.html
+++ b/src/select2/select-multiple.tpl.html
@@ -16,7 +16,7 @@
aria-label="{{ $select.baseTitle }}"
aria-activedescendant="ui-select-choices-row-{{ $select.generatedId }}-{{ $select.activeIndex }}"
class="select2-input ui-select-search"
- placeholder="{{$select.getPlaceholder()}}"
+ placeholder="{{$selectMultiple.getPlaceholder()}}"
ng-disabled="$select.disabled"
ng-hide="$select.disabled"
ng-model="$select.search"
diff --git a/src/uiSelectChoicesDirective.js b/src/uiSelectChoicesDirective.js
index b61bf89b0..41d24e31b 100644
--- a/src/uiSelectChoicesDirective.js
+++ b/src/uiSelectChoicesDirective.js
@@ -1,5 +1,5 @@
uis.directive('uiSelectChoices',
- ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr', '$compile',
+ ['uiSelectConfig', 'uisRepeatParser', 'uiSelectMinErr', '$compile',
function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile) {
return {
diff --git a/src/uiSelectController.js b/src/uiSelectController.js
index 40e2f5486..a2959566f 100644
--- a/src/uiSelectController.js
+++ b/src/uiSelectController.js
@@ -5,7 +5,7 @@
* put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
*/
uis.controller('uiSelectCtrl',
- ['$scope', '$element', '$timeout', '$filter', 'RepeatParser', 'uiSelectMinErr', 'uiSelectConfig',
+ ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig',
function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) {
var ctrl = this;
@@ -13,37 +13,41 @@ uis.controller('uiSelectCtrl',
var EMPTY_SEARCH = '';
ctrl.placeholder = uiSelectConfig.placeholder;
+ ctrl.searchEnabled = uiSelectConfig.searchEnabled;
+ ctrl.sortable = uiSelectConfig.sortable;
+ ctrl.refreshDelay = uiSelectConfig.refreshDelay;
+
+ ctrl.removeSelected = false; //If selected item(s) should be removed from dropdown list
+ ctrl.closeOnSelect = true; //Initialized inside uiSelect directive link function
ctrl.search = EMPTY_SEARCH;
- ctrl.activeIndex = 0;
- ctrl.activeMatchIndex = -1;
- ctrl.items = [];
- ctrl.selected = undefined;
+
+ ctrl.activeIndex = 0; //Dropdown of choices
+ ctrl.items = []; //All available choices
+
ctrl.open = false;
ctrl.focus = false;
- ctrl.focusser = undefined; //Reference to input element used to handle focus events
ctrl.disabled = false;
- ctrl.searchEnabled = uiSelectConfig.searchEnabled;
- ctrl.sortable = uiSelectConfig.sortable;
+ ctrl.selected = undefined;
+
+ ctrl.focusser = undefined; //Reference to input element used to handle focus events
ctrl.resetSearchInput = true;
ctrl.multiple = undefined; // Initialized inside uiSelect directive link function
- ctrl.refreshDelay = uiSelectConfig.refreshDelay;
ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelectChoices directive link function
ctrl.tagging = {isActivated: false, fct: undefined};
ctrl.taggingTokens = {isActivated: false, tokens: undefined};
ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelectMatch directive link function
- ctrl.closeOnSelect = true; // Initialized inside uiSelect directive link function
ctrl.clickTriggeredSelect = false;
ctrl.$filter = $filter;
+ ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
+ if (ctrl.searchInput.length !== 1) {
+ throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
+ }
+
ctrl.isEmpty = function() {
return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === '';
};
- var _searchInput = $element.querySelectorAll('input.ui-select-search');
- if (_searchInput.length !== 1) {
- throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", _searchInput.length);
- }
-
// Most of the time the user does not want to empty the search input when in typeahead mode
function _resetSearchInput() {
if (ctrl.resetSearchInput || (ctrl.resetSearchInput === undefined && uiSelectConfig.resetSearchInput)) {
@@ -59,9 +63,10 @@ uis.controller('uiSelectCtrl',
ctrl.activate = function(initSearchValue, avoidReset) {
if (!ctrl.disabled && !ctrl.open) {
if(!avoidReset) _resetSearchInput();
- ctrl.focusser.prop('disabled', true); //Will reactivate it on .close()
+
+ $scope.$broadcast('uis:activate');
+
ctrl.open = true;
- ctrl.activeMatchIndex = -1;
ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
@@ -74,7 +79,7 @@ uis.controller('uiSelectCtrl',
// Give it time to appear before focus
$timeout(function() {
ctrl.search = initSearchValue || ctrl.search;
- _searchInput[0].focus();
+ ctrl.searchInput[0].focus();
});
}
};
@@ -109,16 +114,29 @@ uis.controller('uiSelectCtrl',
ctrl.items = items;
}
- var setItemsFn = groupByExp ? updateGroups : setPlainItems;
+ ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;
ctrl.parserResult = RepeatParser.parse(repeatAttr);
ctrl.isGrouped = !!groupByExp;
ctrl.itemProperty = ctrl.parserResult.itemName;
+ ctrl.refreshItems = function (data){
+ data = data || ctrl.parserResult.source($scope);
+ var selectedItems = ctrl.selected;
+ //TODO should implement for single mode removeSelected
+ if ((angular.isArray(selectedItems) && !selectedItems.length) || !ctrl.removeSelected) {
+ ctrl.setItemsFn(data);
+ }else{
+ if ( data !== undefined ) {
+ var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
+ ctrl.setItemsFn(filteredItems);
+ }
+ }
+ };
+
// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
$scope.$watchCollection(ctrl.parserResult.source, function(items) {
-
if (items === undefined || items === null) {
// If the user specifies undefined or null => reset the collection
// Special case: items can be undefined if the user did not initialized the collection on the scope
@@ -128,36 +146,14 @@ uis.controller('uiSelectCtrl',
if (!angular.isArray(items)) {
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
- if (ctrl.multiple){
- //Remove already selected items (ex: while searching)
- var filteredItems = items.filter(function(i) {return ctrl.selected.indexOf(i) < 0;});
- setItemsFn(filteredItems);
- }else{
- setItemsFn(items);
- }
+ //Remove already selected items (ex: while searching)
+ //TODO Should add a test
+ ctrl.refreshItems(items);
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
-
}
}
-
});
- if (ctrl.multiple){
- //Remove already selected items
- $scope.$watchCollection('$select.selected', function(selectedItems){
- var data = ctrl.parserResult.source($scope);
- if (!selectedItems.length) {
- setItemsFn(data);
- }else{
- if ( data !== undefined ) {
- var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
- setItemsFn(filteredItems);
- }
- }
- ctrl.sizeSearchInput();
- });
- }
-
};
var _refreshDelayPromise;
@@ -268,16 +264,11 @@ uis.controller('uiSelectCtrl',
}
}
+ $scope.$broadcast('uis:select', item);
+
var locals = {};
locals[ctrl.parserResult.itemName] = item;
- if(ctrl.multiple) {
- ctrl.selected.push(item);
- ctrl.sizeSearchInput();
- } else {
- ctrl.selected = item;
- }
-
$timeout(function(){
ctrl.onSelectCallback($scope, {
$item: item,
@@ -285,7 +276,7 @@ uis.controller('uiSelectCtrl',
});
});
- if (!ctrl.multiple || ctrl.closeOnSelect) {
+ if (ctrl.closeOnSelect) {
ctrl.close(skipFocusser);
}
if ($event && $event.type === 'click') {
@@ -301,17 +292,13 @@ uis.controller('uiSelectCtrl',
if (ctrl.ngModel && ctrl.ngModel.$setTouched) ctrl.ngModel.$setTouched();
_resetSearchInput();
ctrl.open = false;
- if (!ctrl.multiple){
- $timeout(function(){
- ctrl.focusser.prop('disabled', false);
- if (!skipFocusser) ctrl.focusser[0].focus();
- },0,false);
- }
+
+ $scope.$broadcast('uis:close', skipFocusser);
+
};
ctrl.setFocus = function(){
- if (!ctrl.focus && !ctrl.multiple) ctrl.focusser[0].focus();
- if (!ctrl.focus && ctrl.multiple) _searchInput[0].focus();
+ if (!ctrl.focus) ctrl.focusInput[0].focus();
};
ctrl.clear = function($event) {
@@ -342,39 +329,11 @@ uis.controller('uiSelectCtrl',
return isLocked;
};
- // Remove item from multiple select
- ctrl.removeChoice = function(index){
- var removedChoice = ctrl.selected[index];
-
- // if the choice is locked, can't remove it
- if(removedChoice._uiSelectChoiceLocked) return;
-
- var locals = {};
- locals[ctrl.parserResult.itemName] = removedChoice;
-
- ctrl.selected.splice(index, 1);
- ctrl.activeMatchIndex = -1;
- ctrl.sizeSearchInput();
-
- // Give some time for scope propagation.
- $timeout(function(){
- ctrl.onRemoveCallback($scope, {
- $item: removedChoice,
- $model: ctrl.parserResult.modelMapper($scope, locals)
- });
- });
- };
-
- ctrl.getPlaceholder = function(){
- //Refactor single?
- if(ctrl.multiple && ctrl.selected.length) return;
- return ctrl.placeholder;
- };
-
var sizeWatch = null;
ctrl.sizeSearchInput = function() {
- var input = _searchInput[0],
- container = _searchInput.parent().parent()[0],
+
+ var input = ctrl.searchInput[0],
+ container = ctrl.searchInput.parent().parent()[0],
calculateContainerWidth = function() {
// Return the container width only if the search input is visible
return container.clientWidth * !!input.offsetParent;
@@ -385,11 +344,11 @@ uis.controller('uiSelectCtrl',
}
var inputWidth = containerWidth - input.offsetLeft - 10;
if (inputWidth < 50) inputWidth = containerWidth;
- _searchInput.css('width', inputWidth+'px');
+ ctrl.searchInput.css('width', inputWidth+'px');
return true;
};
- _searchInput.css('width', '10px');
+ ctrl.searchInput.css('width', '10px');
$timeout(function() { //Give tags time to render correctly
if (sizeWatch === null && !updateIfVisible(calculateContainerWidth())) {
sizeWatch = $scope.$watch(calculateContainerWidth, function(containerWidth) {
@@ -432,68 +391,8 @@ uis.controller('uiSelectCtrl',
return processed;
}
- // Handles selected options in "multiple" mode
- function _handleMatchSelection(key){
- var caretPosition = _getCaretPosition(_searchInput[0]),
- length = ctrl.selected.length,
- // none = -1,
- first = 0,
- last = length-1,
- curr = ctrl.activeMatchIndex,
- next = ctrl.activeMatchIndex+1,
- prev = ctrl.activeMatchIndex-1,
- newIndex = curr;
-
- if(caretPosition > 0 || (ctrl.search.length && key == KEY.RIGHT)) return false;
-
- ctrl.close();
-
- function getNewActiveMatchIndex(){
- switch(key){
- case KEY.LEFT:
- // Select previous/first item
- if(~ctrl.activeMatchIndex) return prev;
- // Select last item
- else return last;
- break;
- case KEY.RIGHT:
- // Open drop-down
- if(!~ctrl.activeMatchIndex || curr === last){
- ctrl.activate();
- return false;
- }
- // Select next/last item
- else return next;
- break;
- case KEY.BACKSPACE:
- // Remove selected item and select previous/first
- if(~ctrl.activeMatchIndex){
- ctrl.removeChoice(curr);
- return prev;
- }
- // Select last item
- else return last;
- break;
- case KEY.DELETE:
- // Remove selected item and select next item
- if(~ctrl.activeMatchIndex){
- ctrl.removeChoice(ctrl.activeMatchIndex);
- return curr;
- }
- else return false;
- }
- }
-
- newIndex = getNewActiveMatchIndex();
-
- if(!ctrl.selected.length || newIndex === false) ctrl.activeMatchIndex = -1;
- else ctrl.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
-
- return true;
- }
-
// Bind to keyboard shortcuts
- _searchInput.on('keydown', function(e) {
+ ctrl.searchInput.on('keydown', function(e) {
var key = e.which;
@@ -503,15 +402,11 @@ uis.controller('uiSelectCtrl',
// }
$scope.$apply(function() {
- var processed = false;
- var tagged = false;
- if(ctrl.multiple && KEY.isHorizontalMovement(key)){
- processed = _handleMatchSelection(key);
- }
+ var tagged = false;
- if (!processed && (ctrl.items.length > 0 || ctrl.tagging.isActivated)) {
- processed = _handleDropDownSelection(key);
+ if (ctrl.items.length > 0 || ctrl.tagging.isActivated) {
+ _handleDropDownSelection(key);
if ( ctrl.taggingTokens.isActivated ) {
for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) {
if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) {
@@ -523,7 +418,7 @@ uis.controller('uiSelectCtrl',
}
if ( tagged ) {
$timeout(function() {
- _searchInput.triggerHandler('tagged');
+ ctrl.searchInput.triggerHandler('tagged');
var newItem = ctrl.search.replace(KEY.MAP[e.keyCode],'').trim();
if ( ctrl.tagging.fct ) {
newItem = ctrl.tagging.fct( newItem );
@@ -534,12 +429,6 @@ uis.controller('uiSelectCtrl',
}
}
- if (processed && key != KEY.TAB) {
- //TODO Check si el tab selecciona aun correctamente
- //Crear test
- e.preventDefault();
- e.stopPropagation();
- }
});
if(KEY.isVerticalMovement(key) && ctrl.items.length > 0){
@@ -549,7 +438,7 @@ uis.controller('uiSelectCtrl',
});
// If tagging try to split by tokens and add items
- _searchInput.on('paste', function (e) {
+ ctrl.searchInput.on('paste', function (e) {
var data = e.originalEvent.clipboardData.getData('text/plain');
if (data && data.length > 0 && ctrl.taggingTokens.isActivated && ctrl.tagging.fct) {
var items = data.split(ctrl.taggingTokens.tokens[0]); // split by first token only
@@ -566,166 +455,12 @@ uis.controller('uiSelectCtrl',
}
});
- _searchInput.on('keyup', function(e) {
- if ( ! KEY.isVerticalMovement(e.which) ) {
- $scope.$evalAsync( function () {
- ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0;
- });
- }
- // Push a "create new" item into array if there is a search string
- if ( ctrl.tagging.isActivated && ctrl.search.length > 0 ) {
-
- // return early with these keys
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
- return;
- }
- // always reset the activeIndex to the first item when tagging
- ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0;
- // taggingLabel === false bypasses all of this
- if (ctrl.taggingLabel === false) return;
-
- var items = angular.copy( ctrl.items );
- var stashArr = angular.copy( ctrl.items );
- var newItem;
- var item;
- var hasTag = false;
- var dupeIndex = -1;
- var tagItems;
- var tagItem;
-
- // case for object tagging via transform `ctrl.tagging.fct` function
- if ( ctrl.tagging.fct !== undefined) {
- tagItems = ctrl.$filter('filter')(items,{'isTag': true});
- if ( tagItems.length > 0 ) {
- tagItem = tagItems[0];
- }
- // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
- if ( items.length > 0 && tagItem ) {
- hasTag = true;
- items = items.slice(1,items.length);
- stashArr = stashArr.slice(1,stashArr.length);
- }
- newItem = ctrl.tagging.fct(ctrl.search);
- newItem.isTag = true;
- // verify the the tag doesn't match the value of an existing item
- if ( stashArr.filter( function (origItem) { return angular.equals( origItem, ctrl.tagging.fct(ctrl.search) ); } ).length > 0 ) {
- return;
- }
- newItem.isTag = true;
- // handle newItem string and stripping dupes in tagging string context
- } else {
- // find any tagging items already in the ctrl.items array and store them
- tagItems = ctrl.$filter('filter')(items,function (item) {
- return item.match(ctrl.taggingLabel);
- });
- if ( tagItems.length > 0 ) {
- tagItem = tagItems[0];
- }
- item = items[0];
- // remove existing tag item if found (should only ever be one tag item)
- if ( item !== undefined && items.length > 0 && tagItem ) {
- hasTag = true;
- items = items.slice(1,items.length);
- stashArr = stashArr.slice(1,stashArr.length);
- }
- newItem = ctrl.search+' '+ctrl.taggingLabel;
- if ( _findApproxDupe(ctrl.selected, ctrl.search) > -1 ) {
- return;
- }
- // verify the the tag doesn't match the value of an existing item from
- // the searched data set or the items already selected
- if ( _findCaseInsensitiveDupe(stashArr.concat(ctrl.selected)) ) {
- // if there is a tag from prev iteration, strip it / queue the change
- // and return early
- if ( hasTag ) {
- items = stashArr;
- $scope.$evalAsync( function () {
- ctrl.activeIndex = 0;
- ctrl.items = items;
- });
- }
- return;
- }
- if ( _findCaseInsensitiveDupe(stashArr) ) {
- // if there is a tag from prev iteration, strip it
- if ( hasTag ) {
- ctrl.items = stashArr.slice(1,stashArr.length);
- }
- return;
- }
- }
- if ( hasTag ) dupeIndex = _findApproxDupe(ctrl.selected, newItem);
- // dupe found, shave the first item
- if ( dupeIndex > -1 ) {
- items = items.slice(dupeIndex+1,items.length-1);
- } else {
- items = [];
- items.push(newItem);
- items = items.concat(stashArr);
- }
- $scope.$evalAsync( function () {
- ctrl.activeIndex = 0;
- ctrl.items = items;
- });
- }
- });
-
- _searchInput.on('tagged', function() {
+ ctrl.searchInput.on('tagged', function() {
$timeout(function() {
_resetSearchInput();
});
});
- _searchInput.on('blur', function() {
- $timeout(function() {
- ctrl.activeMatchIndex = -1;
- });
- });
-
- function _findCaseInsensitiveDupe(arr) {
- if ( arr === undefined || ctrl.search === undefined ) {
- return false;
- }
- var hasDupe = arr.filter( function (origItem) {
- if ( ctrl.search.toUpperCase() === undefined || origItem === undefined ) {
- return false;
- }
- return origItem.toUpperCase() === ctrl.search.toUpperCase();
- }).length > 0;
-
- return hasDupe;
- }
-
- function _findApproxDupe(haystack, needle) {
- var dupeIndex = -1;
- if(angular.isArray(haystack)) {
- var tempArr = angular.copy(haystack);
- for (var i = 0; i model
- ngModel.$parsers.unshift(function (inputValue) {
- var locals = {},
- result;
- if ($select.multiple){
- var resultMultiple = [];
- for (var j = $select.selected.length - 1; j >= 0; j--) {
- locals = {};
- locals[$select.parserResult.itemName] = $select.selected[j];
- result = $select.parserResult.modelMapper(scope, locals);
- resultMultiple.unshift(result);
- }
- return resultMultiple;
- }else{
- locals = {};
- locals[$select.parserResult.itemName] = inputValue;
- result = $select.parserResult.modelMapper(scope, locals);
- return result;
- }
- });
-
- //From model --> view
- ngModel.$formatters.unshift(function (inputValue) {
- var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
- locals = {},
- result;
- if (data){
- if ($select.multiple){
- var resultMultiple = [];
- var checkFnMultiple = function(list, value){
- //if the list is empty add the value to the list
- if (!list || !list.length){
- resultMultiple.unshift(value);
- return true;
- }
- for (var p = list.length - 1; p >= 0; p--) {
- locals[$select.parserResult.itemName] = list[p];
- result = $select.parserResult.modelMapper(scope, locals);
- if($select.parserResult.trackByExp){
- var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
- if(matches.length>0 && result[matches[1]] == value[matches[1]]){
- resultMultiple.unshift(list[p]);
- return true;
- }
- }
- if (result == value){
- resultMultiple.unshift(list[p]);
- return true;
- }
- }
- return false;
- };
- if (!inputValue) return resultMultiple; //If ngModel was undefined
- for (var k = inputValue.length - 1; k >= 0; k--) {
- if (!checkFnMultiple($select.selected, inputValue[k])){
- checkFnMultiple(data, inputValue[k]);
- }
- }
- return resultMultiple;
- }else{
- var checkFnSingle = function(d){
- locals[$select.parserResult.itemName] = d;
- result = $select.parserResult.modelMapper(scope, locals);
- return result == inputValue;
- };
- //If possible pass same object stored in $select.selected
- if ($select.selected && checkFnSingle($select.selected)) {
- return $select.selected;
- }
- for (var i = data.length - 1; i >= 0; i--) {
- if (checkFnSingle(data[i])) return data[i];
- }
- }
- }
- return inputValue;
- });
+ compile: function(tElement, tAttrs) {
- //Set reference to ngModel from uiSelectCtrl
- $select.ngModel = ngModel;
+ //Multiple or Single depending if multiple attribute presence
+ if (angular.isDefined(tAttrs.multiple))
+ tElement.append("").removeAttr('multiple');
+ else
+ tElement.append("");
- $select.choiceGrouped = function(group){
- return $select.isGrouped && group && group.name;
- };
+ return function(scope, element, attrs, ctrls, transcludeFn) {
+
+ var $select = ctrls[0];
+ var ngModel = ctrls[1];
- //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
- var focusser = angular.element("");
+ $select.generatedId = uiSelectConfig.generateId();
+ $select.baseTitle = attrs.title || 'Select box';
+ $select.focusserTitle = $select.baseTitle + ' focus';
+ $select.focusserId = 'focusser-' + $select.generatedId;
- if(attrs.tabindex){
- //tabindex might be an expression, wait until it contains the actual value before we set the focusser tabindex
- attrs.$observe('tabindex', function(value) {
- //If we are using multiple, add tabindex to the search input
- if($select.multiple){
- searchInput.attr("tabindex", value);
+ $select.closeOnSelect = function() {
+ if (angular.isDefined(attrs.closeOnSelect)) {
+ return $parse(attrs.closeOnSelect)();
} else {
- focusser.attr("tabindex", value);
+ return uiSelectConfig.closeOnSelect;
}
- //Remove the tabindex on the parent so that it is not focusable
- element.removeAttr("tabindex");
- });
- }
+ }();
+
+ $select.onSelectCallback = $parse(attrs.onSelect);
+ $select.onRemoveCallback = $parse(attrs.onRemove);
+
+ //Set reference to ngModel from uiSelectCtrl
+ $select.ngModel = ngModel;
+
+ $select.choiceGrouped = function(group){
+ return $select.isGrouped && group && group.name;
+ };
+
+ if(attrs.tabindex){
+ attrs.$observe('tabindex', function(value) {
+ $select.focusInput.attr("tabindex", value);
+ element.removeAttr("tabindex");
+ });
+ }
- $compile(focusser)(scope);
- $select.focusser = focusser;
+ scope.$watch('searchEnabled', function() {
+ var searchEnabled = scope.$eval(attrs.searchEnabled);
+ $select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
+ });
- if (!$select.multiple){
+ scope.$watch('sortable', function() {
+ var sortable = scope.$eval(attrs.sortable);
+ $select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
+ });
- element.append(focusser);
- focusser.bind("focus", function(){
- scope.$evalAsync(function(){
- $select.focus = true;
- });
+ attrs.$observe('disabled', function() {
+ // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
+ $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
});
- focusser.bind("blur", function(){
- scope.$evalAsync(function(){
- $select.focus = false;
- });
+
+ attrs.$observe('resetSearchInput', function() {
+ // $eval() is needed otherwise we get a string instead of a boolean
+ var resetSearchInput = scope.$eval(attrs.resetSearchInput);
+ $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
});
- focusser.bind("keydown", function(e){
- if (e.which === KEY.BACKSPACE) {
- e.preventDefault();
- e.stopPropagation();
- $select.select(undefined);
- scope.$apply();
- return;
+ attrs.$observe('tagging', function() {
+ if(attrs.tagging !== undefined)
+ {
+ // $eval() is needed otherwise we get a string instead of a boolean
+ var taggingEval = scope.$eval(attrs.tagging);
+ $select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined};
}
-
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
- return;
+ else
+ {
+ $select.tagging = {isActivated: false, fct: undefined};
}
+ });
- if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){
- e.preventDefault();
- e.stopPropagation();
- $select.activate();
+ attrs.$observe('taggingLabel', function() {
+ if(attrs.tagging !== undefined )
+ {
+ // check eval for FALSE, in this case, we disable the labels
+ // associated with tagging
+ if ( attrs.taggingLabel === 'false' ) {
+ $select.taggingLabel = false;
+ }
+ else
+ {
+ $select.taggingLabel = attrs.taggingLabel !== undefined ? attrs.taggingLabel : '(new)';
+ }
}
-
- scope.$digest();
});
- focusser.bind("keyup input", function(e){
-
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
- return;
+ attrs.$observe('taggingTokens', function() {
+ if (attrs.tagging !== undefined) {
+ var tokens = attrs.taggingTokens !== undefined ? attrs.taggingTokens.split('|') : [',','ENTER'];
+ $select.taggingTokens = {isActivated: true, tokens: tokens };
}
-
- $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
- focusser.val('');
- scope.$digest();
-
});
- }
-
-
- scope.$watch('searchEnabled', function() {
- var searchEnabled = scope.$eval(attrs.searchEnabled);
- $select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
- });
-
- scope.$watch('sortable', function() {
- var sortable = scope.$eval(attrs.sortable);
- $select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
- });
+ //Automatically gets focus when loaded
+ if (angular.isDefined(attrs.autofocus)){
+ $timeout(function(){
+ $select.setFocus();
+ });
+ }
- attrs.$observe('disabled', function() {
- // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
- $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
- if ($select.multiple) {
- // As the search input field may now become visible, it may be necessary to recompute its size
- $select.sizeSearchInput();
+ //Gets focus based on scope event name (e.g. focus-on='SomeEventName')
+ if (angular.isDefined(attrs.focusOn)){
+ scope.$on(attrs.focusOn, function() {
+ $timeout(function(){
+ $select.setFocus();
+ });
+ });
}
- });
- attrs.$observe('resetSearchInput', function() {
- // $eval() is needed otherwise we get a string instead of a boolean
- var resetSearchInput = scope.$eval(attrs.resetSearchInput);
- $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
- });
+ function onDocumentClick(e) {
+ if (!$select.open) return; //Skip it if dropdown is close
- attrs.$observe('tagging', function() {
- if(attrs.tagging !== undefined)
- {
- // $eval() is needed otherwise we get a string instead of a boolean
- var taggingEval = scope.$eval(attrs.tagging);
- $select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined};
- }
- else
- {
- $select.tagging = {isActivated: false, fct: undefined};
- }
- });
-
- attrs.$observe('taggingLabel', function() {
- if(attrs.tagging !== undefined )
- {
- // check eval for FALSE, in this case, we disable the labels
- // associated with tagging
- if ( attrs.taggingLabel === 'false' ) {
- $select.taggingLabel = false;
+ var contains = false;
+
+ if (window.jQuery) {
+ // Firefox 3.6 does not support element.contains()
+ // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
+ contains = window.jQuery.contains(element[0], e.target);
+ } else {
+ contains = element[0].contains(e.target);
}
- else
- {
- $select.taggingLabel = attrs.taggingLabel !== undefined ? attrs.taggingLabel : '(new)';
+
+ if (!contains && !$select.clickTriggeredSelect) {
+ //Will lose focus only with certain targets
+ var focusableControls = ['input','button','textarea'];
+ var targetScope = angular.element(e.target).scope(); //To check if target is other ui-select
+ var skipFocusser = targetScope && targetScope.$select && targetScope.$select !== $select; //To check if target is other ui-select
+ if (!skipFocusser) skipFocusser = ~focusableControls.indexOf(e.target.tagName.toLowerCase()); //Check if target is input, button or textarea
+ $select.close(skipFocusser);
+ scope.$digest();
}
+ $select.clickTriggeredSelect = false;
}
- });
- attrs.$observe('taggingTokens', function() {
- if (attrs.tagging !== undefined) {
- var tokens = attrs.taggingTokens !== undefined ? attrs.taggingTokens.split('|') : [',','ENTER'];
- $select.taggingTokens = {isActivated: true, tokens: tokens };
- }
- });
+ // See Click everywhere but here event http://stackoverflow.com/questions/12931369
+ $document.on('click', onDocumentClick);
- //Automatically gets focus when loaded
- if (angular.isDefined(attrs.autofocus)){
- $timeout(function(){
- $select.setFocus();
- });
- }
-
- //Gets focus based on scope event name (e.g. focus-on='SomeEventName')
- if (angular.isDefined(attrs.focusOn)){
- scope.$on(attrs.focusOn, function() {
- $timeout(function(){
- $select.setFocus();
- });
+ scope.$on('$destroy', function() {
+ $document.off('click', onDocumentClick);
});
- }
- if ($select.multiple){
- scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
- if (oldValue != newValue)
- ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
- });
- $select.firstPass = true; // so the form doesn't get dirty as soon as it loads
- scope.$watchCollection('$select.selected', function() {
- if (!$select.firstPass) {
- ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
- } else {
- $select.firstPass = false;
- }
- });
- focusser.prop('disabled', true); //Focusser isn't needed if multiple
- }else{
- scope.$watch('$select.selected', function(newValue) {
- if (ngModel.$viewValue !== newValue) {
- ngModel.$setViewValue(newValue);
- }
- });
- }
-
- ngModel.$render = function() {
- if($select.multiple){
- // Make sure that model value is array
- if(!angular.isArray(ngModel.$viewValue)){
- // Have tolerance for null or undefined values
- if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){
- $select.selected = [];
+ // Support for appending the select field to the body when its open
+ var appendToBody = scope.$eval(attrs.appendToBody);
+ if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
+ scope.$watch('$select.open', function(isOpen) {
+ if (isOpen) {
+ positionDropdown();
} else {
- throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
+ resetDropdown();
}
- }
+ });
+
+ // Move the dropdown back to its original location when the scope is destroyed. Otherwise
+ // it might stick around when the user routes away or the select field is otherwise removed
+ scope.$on('$destroy', function() {
+ resetDropdown();
+ });
}
- $select.selected = ngModel.$viewValue;
- };
- function onDocumentClick(e) {
- if (!$select.open) return; //Skip it if dropdown is close
+ // Hold on to a reference to the .ui-select-container element for appendToBody support
+ var placeholder = null;
- var contains = false;
+ function positionDropdown() {
+ // Remember the absolute position of the element
+ var offset = uisOffset(element);
- if (window.jQuery) {
- // Firefox 3.6 does not support element.contains()
- // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
- contains = window.jQuery.contains(element[0], e.target);
- } else {
- contains = element[0].contains(e.target);
- }
+ // Clone the element into a placeholder element to take its original place in the DOM
+ placeholder = angular.element('');
+ placeholder[0].style.width = offset.width + 'px';
+ placeholder[0].style.height = offset.height + 'px';
+ element.after(placeholder);
- if (!contains && !$select.clickTriggeredSelect) {
- //Will lose focus only with certain targets
- var focusableControls = ['input','button','textarea'];
- var targetScope = angular.element(e.target).scope(); //To check if target is other ui-select
- var skipFocusser = targetScope && targetScope.$select && targetScope.$select !== $select; //To check if target is other ui-select
- if (!skipFocusser) skipFocusser = ~focusableControls.indexOf(e.target.tagName.toLowerCase()); //Check if target is input, button or textarea
- $select.close(skipFocusser);
- scope.$digest();
- }
- $select.clickTriggeredSelect = false;
- }
-
- // See Click everywhere but here event http://stackoverflow.com/questions/12931369
- $document.on('click', onDocumentClick);
-
- scope.$on('$destroy', function() {
- $document.off('click', onDocumentClick);
- });
-
- // Move transcluded elements to their correct position in main template
- transcludeFn(scope, function(clone) {
- // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
-
- // One day jqLite will be replaced by jQuery and we will be able to write:
- // var transcludedElement = clone.filter('.my-class')
- // instead of creating a hackish DOM element:
- var transcluded = angular.element('
').append(clone);
-
- var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
- transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr
- transcludedMatch.removeAttr('data-ui-select-match'); // Properly handle HTML5 data-attributes
- if (transcludedMatch.length !== 1) {
- throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
- }
- element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);
+ // Now move the actual dropdown element to the end of the body
+ $document.find('body').append(element);
- var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
- transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr
- transcludedChoices.removeAttr('data-ui-select-choices'); // Properly handle HTML5 data-attributes
- if (transcludedChoices.length !== 1) {
- throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
+ element[0].style.position = 'absolute';
+ element[0].style.left = offset.left + 'px';
+ element[0].style.top = offset.top + 'px';
+ element[0].style.width = offset.width + 'px';
}
- element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
- });
-
- // Support for appending the select field to the body when its open
- var appendToBody = scope.$eval(attrs.appendToBody);
- if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
- scope.$watch('$select.open', function(isOpen) {
- if (isOpen) {
- positionDropdown();
- } else {
- resetDropdown();
+
+ function resetDropdown() {
+ if (placeholder === null) {
+ // The dropdown has not actually been display yet, so there's nothing to reset
+ return;
}
- });
- // Move the dropdown back to its original location when the scope is destroyed. Otherwise
- // it might stick around when the user routes away or the select field is otherwise removed
- scope.$on('$destroy', function() {
- resetDropdown();
- });
- }
-
- // Hold on to a reference to the .ui-select-container element for appendToBody support
- var placeholder = null;
-
- function positionDropdown() {
- // Remember the absolute position of the element
- var offset = uisOffset(element);
-
- // Clone the element into a placeholder element to take its original place in the DOM
- placeholder = angular.element('');
- placeholder[0].style.width = offset.width + 'px';
- placeholder[0].style.height = offset.height + 'px';
- element.after(placeholder);
-
- // Now move the actual dropdown element to the end of the body
- $document.find('body').append(element);
-
- element[0].style.position = 'absolute';
- element[0].style.left = offset.left + 'px';
- element[0].style.top = offset.top + 'px';
- element[0].style.width = offset.width + 'px';
- }
-
- function resetDropdown() {
- if (placeholder === null) {
- // The dropdown has not actually been display yet, so there's nothing to reset
- return;
+ // Move the dropdown element back to its original location in the DOM
+ placeholder.replaceWith(element);
+ placeholder = null;
+
+ element[0].style.position = '';
+ element[0].style.left = '';
+ element[0].style.top = '';
+ element[0].style.width = '';
}
- // Move the dropdown element back to its original location in the DOM
- placeholder.replaceWith(element);
- placeholder = null;
+ // Move transcluded elements to their correct position in main template
+ transcludeFn(scope, function(clone) {
+ // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
+
+ // One day jqLite will be replaced by jQuery and we will be able to write:
+ // var transcludedElement = clone.filter('.my-class')
+ // instead of creating a hackish DOM element:
+ var transcluded = angular.element('
').append(clone);
- element[0].style.position = '';
- element[0].style.left = '';
- element[0].style.top = '';
- element[0].style.width = '';
- }
+ var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
+ transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr
+ transcludedMatch.removeAttr('data-ui-select-match'); // Properly handle HTML5 data-attributes
+ if (transcludedMatch.length !== 1) {
+ throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
+ }
+ element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);
+
+ var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
+ transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr
+ transcludedChoices.removeAttr('data-ui-select-choices'); // Properly handle HTML5 data-attributes
+ if (transcludedChoices.length !== 1) {
+ throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
+ }
+ element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
+ });
+ };
}
};
}]);
diff --git a/src/uiSelectMultipleDirective.js b/src/uiSelectMultipleDirective.js
new file mode 100644
index 000000000..dd1c89723
--- /dev/null
+++ b/src/uiSelectMultipleDirective.js
@@ -0,0 +1,393 @@
+uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelectMinErr, $timeout) {
+ return {
+ restrict: 'EA',
+ require: ['^uiSelect', '^ngModel'],
+
+ controller: ['$scope','$timeout', function($scope, $timeout){
+
+ var ctrl = this;
+ var $select = $scope.$select;
+
+ ctrl.activeMatchIndex = -1;
+
+ // Remove item from multiple select
+ ctrl.removeChoice = function(index){
+
+ var removedChoice = $select.selected[index];
+
+ // if the choice is locked, can't remove it
+ if(removedChoice._uiSelectChoiceLocked) return;
+
+ var locals = {};
+ locals[$select.parserResult.itemName] = removedChoice;
+
+ $select.selected.splice(index, 1);
+ ctrl.activeMatchIndex = -1;
+ $select.sizeSearchInput();
+
+ // Give some time for scope propagation.
+ $timeout(function(){
+ $select.onRemoveCallback($scope, {
+ $item: removedChoice,
+ $model: $select.parserResult.modelMapper($scope, locals)
+ });
+ });
+
+ };
+
+ ctrl.getPlaceholder = function(){
+ //Refactor single?
+ if($select.selected.length) return;
+ return $select.placeholder;
+ };
+
+
+ }],
+ controllerAs: '$selectMultiple',
+
+ link: function(scope, element, attrs, ctrls) {
+
+ var $select = ctrls[0];
+ var ngModel = ctrls[1];
+ var $selectMultiple = scope.$selectMultiple;
+
+ $select.multiple = true;
+ $select.removeSelected = true;
+
+ //Input that will handle focus
+ $select.focusInput = $select.searchInput;
+
+ //From view --> model
+ ngModel.$parsers.unshift(function () {
+ var locals = {},
+ result,
+ resultMultiple = [];
+ for (var j = $select.selected.length - 1; j >= 0; j--) {
+ locals = {};
+ locals[$select.parserResult.itemName] = $select.selected[j];
+ result = $select.parserResult.modelMapper(scope, locals);
+ resultMultiple.unshift(result);
+ }
+ return resultMultiple;
+ });
+
+ // From model --> view
+ ngModel.$formatters.unshift(function (inputValue) {
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
+ locals = {},
+ result;
+ if (data){
+ var resultMultiple = [];
+ var checkFnMultiple = function(list, value){
+ //if the list is empty add the value to the list
+ if (!list || !list.length){
+ resultMultiple.unshift(value);
+ return true;
+ }
+ for (var p = list.length - 1; p >= 0; p--) {
+ locals[$select.parserResult.itemName] = list[p];
+ result = $select.parserResult.modelMapper(scope, locals);
+ if($select.parserResult.trackByExp){
+ var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
+ if(matches.length>0 && result[matches[1]] == value[matches[1]]){
+ resultMultiple.unshift(list[p]);
+ return true;
+ }
+ }
+ if (result == value){
+ resultMultiple.unshift(list[p]);
+ return true;
+ }
+ }
+ return false;
+ };
+ if (!inputValue) return resultMultiple; //If ngModel was undefined
+ for (var k = inputValue.length - 1; k >= 0; k--) {
+ if (!checkFnMultiple($select.selected, inputValue[k])){
+ checkFnMultiple(data, inputValue[k]);
+ }
+ }
+ return resultMultiple;
+ }
+ return inputValue;
+ });
+
+ //Watch selection
+ scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
+ if (oldValue != newValue)
+ ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
+ });
+ //TODO Should be a better way to detect first pass
+ $select.firstPass = true; // so the form doesn't get dirty as soon as it loads
+ scope.$watchCollection('$select.selected', function() {
+ if (!$select.firstPass) {
+ ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
+ } else {
+ $select.firstPass = false;
+ }
+ //Remove already selected items
+ //e.g. When user clicks on a selection, the selected array changes and
+ //the dropdown should remove that item
+ $select.refreshItems();
+ //TODO Should add a test
+ $select.sizeSearchInput();
+ });
+
+ ngModel.$render = function() {
+ // Make sure that model value is array
+ if(!angular.isArray(ngModel.$viewValue)){
+ // Have tolerance for null or undefined values
+ if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){
+ $select.selected = [];
+ } else {
+ throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
+ }
+ }
+ $select.selected = ngModel.$viewValue;
+ };
+
+ scope.$on('uis:select', function (event, item) {
+ $select.selected.push(item);
+ $select.sizeSearchInput();
+ });
+
+ scope.$on('uis:activate', function () {
+ $selectMultiple.activeMatchIndex = -1;
+ });
+
+ scope.$watch('$select.disabled', function(newValue, oldValue) {
+ // As the search input field may now become visible, it may be necessary to recompute its size
+ if (oldValue && !newValue) $select.sizeSearchInput();
+ });
+
+ $select.searchInput.on('keydown', function(e) {
+ var key = e.which;
+ scope.$apply(function() {
+ var processed = false;
+ // var tagged = false; //Checkme
+ if(KEY.isHorizontalMovement(key)){
+ processed = _handleMatchSelection(key);
+ }
+ if (processed && key != KEY.TAB) {
+ //TODO Check si el tab selecciona aun correctamente
+ //Crear test
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+ });
+ function _getCaretPosition(el) {
+ if(angular.isNumber(el.selectionStart)) return el.selectionStart;
+ // selectionStart is not supported in IE8 and we don't want hacky workarounds so we compromise
+ else return el.value.length;
+ }
+ // Handles selected options in "multiple" mode
+ function _handleMatchSelection(key){
+ var caretPosition = _getCaretPosition($select.searchInput[0]),
+ length = $select.selected.length,
+ // none = -1,
+ first = 0,
+ last = length-1,
+ curr = $selectMultiple.activeMatchIndex,
+ next = $selectMultiple.activeMatchIndex+1,
+ prev = $selectMultiple.activeMatchIndex-1,
+ newIndex = curr;
+
+ if(caretPosition > 0 || ($select.search.length && key == KEY.RIGHT)) return false;
+
+ $select.close();
+
+ function getNewActiveMatchIndex(){
+ switch(key){
+ case KEY.LEFT:
+ // Select previous/first item
+ if(~$selectMultiple.activeMatchIndex) return prev;
+ // Select last item
+ else return last;
+ break;
+ case KEY.RIGHT:
+ // Open drop-down
+ if(!~$selectMultiple.activeMatchIndex || curr === last){
+ $select.activate();
+ return false;
+ }
+ // Select next/last item
+ else return next;
+ break;
+ case KEY.BACKSPACE:
+ // Remove selected item and select previous/first
+ if(~$selectMultiple.activeMatchIndex){
+ $selectMultiple.removeChoice(curr);
+ return prev;
+ }
+ // Select last item
+ else return last;
+ break;
+ case KEY.DELETE:
+ // Remove selected item and select next item
+ if(~$selectMultiple.activeMatchIndex){
+ $selectMultiple.removeChoice($selectMultiple.activeMatchIndex);
+ return curr;
+ }
+ else return false;
+ }
+ }
+
+ newIndex = getNewActiveMatchIndex();
+
+ if(!$select.selected.length || newIndex === false) $selectMultiple.activeMatchIndex = -1;
+ else $selectMultiple.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
+
+ return true;
+ }
+
+ $select.searchInput.on('keyup', function(e) {
+
+ if ( ! KEY.isVerticalMovement(e.which) ) {
+ scope.$evalAsync( function () {
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
+ });
+ }
+ // Push a "create new" item into array if there is a search string
+ if ( $select.tagging.isActivated && $select.search.length > 0 ) {
+
+ // return early with these keys
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
+ return;
+ }
+ // always reset the activeIndex to the first item when tagging
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
+ // taggingLabel === false bypasses all of this
+ if ($select.taggingLabel === false) return;
+
+ var items = angular.copy( $select.items );
+ var stashArr = angular.copy( $select.items );
+ var newItem;
+ var item;
+ var hasTag = false;
+ var dupeIndex = -1;
+ var tagItems;
+ var tagItem;
+
+ // case for object tagging via transform `$select.tagging.fct` function
+ if ( $select.tagging.fct !== undefined) {
+ tagItems = $select.$filter('filter')(items,{'isTag': true});
+ if ( tagItems.length > 0 ) {
+ tagItem = tagItems[0];
+ }
+ // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
+ if ( items.length > 0 && tagItem ) {
+ hasTag = true;
+ items = items.slice(1,items.length);
+ stashArr = stashArr.slice(1,stashArr.length);
+ }
+ newItem = $select.tagging.fct($select.search);
+ newItem.isTag = true;
+ // verify the the tag doesn't match the value of an existing item
+ if ( stashArr.filter( function (origItem) { return angular.equals( origItem, $select.tagging.fct($select.search) ); } ).length > 0 ) {
+ return;
+ }
+ newItem.isTag = true;
+ // handle newItem string and stripping dupes in tagging string context
+ } else {
+ // find any tagging items already in the $select.items array and store them
+ tagItems = $select.$filter('filter')(items,function (item) {
+ return item.match($select.taggingLabel);
+ });
+ if ( tagItems.length > 0 ) {
+ tagItem = tagItems[0];
+ }
+ item = items[0];
+ // remove existing tag item if found (should only ever be one tag item)
+ if ( item !== undefined && items.length > 0 && tagItem ) {
+ hasTag = true;
+ items = items.slice(1,items.length);
+ stashArr = stashArr.slice(1,stashArr.length);
+ }
+ newItem = $select.search+' '+$select.taggingLabel;
+ if ( _findApproxDupe($select.selected, $select.search) > -1 ) {
+ return;
+ }
+ // verify the the tag doesn't match the value of an existing item from
+ // the searched data set or the items already selected
+ if ( _findCaseInsensitiveDupe(stashArr.concat($select.selected)) ) {
+ // if there is a tag from prev iteration, strip it / queue the change
+ // and return early
+ if ( hasTag ) {
+ items = stashArr;
+ scope.$evalAsync( function () {
+ $select.activeIndex = 0;
+ $select.items = items;
+ });
+ }
+ return;
+ }
+ if ( _findCaseInsensitiveDupe(stashArr) ) {
+ // if there is a tag from prev iteration, strip it
+ if ( hasTag ) {
+ $select.items = stashArr.slice(1,stashArr.length);
+ }
+ return;
+ }
+ }
+ if ( hasTag ) dupeIndex = _findApproxDupe($select.selected, newItem);
+ // dupe found, shave the first item
+ if ( dupeIndex > -1 ) {
+ items = items.slice(dupeIndex+1,items.length-1);
+ } else {
+ items = [];
+ items.push(newItem);
+ items = items.concat(stashArr);
+ }
+ scope.$evalAsync( function () {
+ $select.activeIndex = 0;
+ $select.items = items;
+ });
+ }
+ });
+ function _findCaseInsensitiveDupe(arr) {
+ if ( arr === undefined || $select.search === undefined ) {
+ return false;
+ }
+ var hasDupe = arr.filter( function (origItem) {
+ if ( $select.search.toUpperCase() === undefined || origItem === undefined ) {
+ return false;
+ }
+ return origItem.toUpperCase() === $select.search.toUpperCase();
+ }).length > 0;
+
+ return hasDupe;
+ }
+ function _findApproxDupe(haystack, needle) {
+ var dupeIndex = -1;
+ if(angular.isArray(haystack)) {
+ var tempArr = angular.copy(haystack);
+ for (var i = 0; i model
+ ngModel.$parsers.unshift(function (inputValue) {
+ var locals = {},
+ result;
+ locals[$select.parserResult.itemName] = inputValue;
+ result = $select.parserResult.modelMapper(scope, locals);
+ return result;
+ });
+
+ //From model --> view
+ ngModel.$formatters.unshift(function (inputValue) {
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
+ locals = {},
+ result;
+ if (data){
+ var checkFnSingle = function(d){
+ locals[$select.parserResult.itemName] = d;
+ result = $select.parserResult.modelMapper(scope, locals);
+ return result == inputValue;
+ };
+ //If possible pass same object stored in $select.selected
+ if ($select.selected && checkFnSingle($select.selected)) {
+ return $select.selected;
+ }
+ for (var i = data.length - 1; i >= 0; i--) {
+ if (checkFnSingle(data[i])) return data[i];
+ }
+ }
+ return inputValue;
+ });
+
+ //Update viewValue if model change
+ scope.$watch('$select.selected', function(newValue) {
+ if (ngModel.$viewValue !== newValue) {
+ ngModel.$setViewValue(newValue);
+ }
+ });
+
+ ngModel.$render = function() {
+ $select.selected = ngModel.$viewValue;
+ };
+
+ scope.$on('uis:select', function (event, item) {
+ $select.selected = item;
+ });
+
+ scope.$on('uis:close', function (event, skipFocusser) {
+ $timeout(function(){
+ $select.focusser.prop('disabled', false);
+ if (!skipFocusser) $select.focusser[0].focus();
+ },0,false);
+ });
+
+ scope.$on('uis:activate', function () {
+ focusser.prop('disabled', true); //Will reactivate it on .close()
+ });
+
+ //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
+ var focusser = angular.element("");
+ $compile(focusser)(scope);
+ $select.focusser = focusser;
+
+ //Input that will handle focus
+ $select.focusInput = focusser;
+
+ element.parent().append(focusser);
+ focusser.bind("focus", function(){
+ scope.$evalAsync(function(){
+ $select.focus = true;
+ });
+ });
+ focusser.bind("blur", function(){
+ scope.$evalAsync(function(){
+ $select.focus = false;
+ });
+ });
+ focusser.bind("keydown", function(e){
+
+ if (e.which === KEY.BACKSPACE) {
+ e.preventDefault();
+ e.stopPropagation();
+ $select.select(undefined);
+ scope.$apply();
+ return;
+ }
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
+ return;
+ }
+
+ if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){
+ e.preventDefault();
+ e.stopPropagation();
+ $select.activate();
+ }
+
+ scope.$digest();
+ });
+
+ focusser.bind("keyup input", function(e){
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
+ return;
+ }
+
+ $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
+ focusser.val('');
+ scope.$digest();
+
+ });
+
+
+ }
+ };
+}]);
\ No newline at end of file
diff --git a/src/RepeatParserService.js b/src/uisRepeatParserService.js
similarity index 94%
rename from src/RepeatParserService.js
rename to src/uisRepeatParserService.js
index 938227fb9..7ed9955f1 100644
--- a/src/RepeatParserService.js
+++ b/src/uisRepeatParserService.js
@@ -8,7 +8,7 @@
* https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
*/
-uis.service('RepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
+uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
var self = this;
/**
diff --git a/test/select.spec.js b/test/select.spec.js
index 94581faba..2bcefb2a8 100644
--- a/test/select.spec.js
+++ b/test/select.spec.js
@@ -1286,7 +1286,7 @@ describe('ui-select tests', function() {
expect(isDropdownOpened(el)).toEqual(false);
triggerKeydown(searchInput, Key.Backspace);
expect(isDropdownOpened(el)).toEqual(false);
- expect(el.scope().$select.activeMatchIndex).toBe(el.scope().$select.selected.length - 1);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(el.scope().$select.selected.length - 1);
});
@@ -1302,7 +1302,7 @@ describe('ui-select tests', function() {
triggerKeydown(searchInput, Key.Backspace);
expect(el.scope().$select.selected).toEqual([scope.people[4], scope.people[6]]); //Wladimir & Nicole
- expect(el.scope().$select.activeMatchIndex).toBe(0);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(0);
});
@@ -1318,7 +1318,7 @@ describe('ui-select tests', function() {
triggerKeydown(searchInput, Key.Delete);
expect(el.scope().$select.selected).toEqual([scope.people[4], scope.people[6]]); //Wladimir & Nicole
- expect(el.scope().$select.activeMatchIndex).toBe(1);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(1);
});
@@ -1330,7 +1330,7 @@ describe('ui-select tests', function() {
expect(isDropdownOpened(el)).toEqual(false);
triggerKeydown(searchInput, Key.Left);
expect(isDropdownOpened(el)).toEqual(false);
- expect(el.scope().$select.activeMatchIndex).toBe(el.scope().$select.selected.length - 1);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(el.scope().$select.selected.length - 1);
});
@@ -1344,37 +1344,37 @@ describe('ui-select tests', function() {
triggerKeydown(searchInput, Key.Left)
triggerKeydown(searchInput, Key.Left)
expect(isDropdownOpened(el)).toEqual(false);
- expect(el.scope().$select.activeMatchIndex).toBe(el.scope().$select.selected.length - 2);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(el.scope().$select.selected.length - 2);
triggerKeydown(searchInput, Key.Left)
triggerKeydown(searchInput, Key.Left)
triggerKeydown(searchInput, Key.Left)
- expect(el.scope().$select.activeMatchIndex).toBe(0);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(0);
});
- it('should decrease $select.activeMatchIndex when pressing LEFT key', function() {
+ it('should decrease $selectMultiple.activeMatchIndex when pressing LEFT key', function() {
scope.selection.selectedMultiple = [scope.people[4], scope.people[5], scope.people[6]]; //Wladimir, Samantha & Nicole
var el = createUiSelectMultiple();
var searchInput = el.find('.ui-select-search');
- el.scope().$select.activeMatchIndex = 3
+ el.scope().$selectMultiple.activeMatchIndex = 3
triggerKeydown(searchInput, Key.Left)
triggerKeydown(searchInput, Key.Left)
- expect(el.scope().$select.activeMatchIndex).toBe(1);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(1);
});
- it('should increase $select.activeMatchIndex when pressing RIGHT key', function() {
+ it('should increase $selectMultiple.activeMatchIndex when pressing RIGHT key', function() {
scope.selection.selectedMultiple = [scope.people[4], scope.people[5], scope.people[6]]; //Wladimir, Samantha & Nicole
var el = createUiSelectMultiple();
var searchInput = el.find('.ui-select-search');
- el.scope().$select.activeMatchIndex = 0
+ el.scope().$selectMultiple.activeMatchIndex = 0
triggerKeydown(searchInput, Key.Right)
triggerKeydown(searchInput, Key.Right)
- expect(el.scope().$select.activeMatchIndex).toBe(2);
+ expect(el.scope().$selectMultiple.activeMatchIndex).toBe(2);
});