UNPKG

ui-select

Version:
461 lines (411 loc) 16.9 kB
uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelectMinErr, $timeout) { return { restrict: 'EA', require: ['^uiSelect', '^ngModel'], controller: ['$scope','$timeout', function($scope, $timeout){ var ctrl = this, $select = $scope.$select, ngModel; if (angular.isUndefined($select.selected)) $select.selected = []; //Wait for link fn to inject it $scope.$evalAsync(function(){ ngModel = $scope.ngModel; }); ctrl.activeMatchIndex = -1; ctrl.updateModel = function(){ ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes ctrl.refreshComponent(); }; ctrl.refreshComponent = function(){ //Remove already selected items //e.g. When user clicks on a selection, the selected array changes and //the dropdown should remove that item if($select.refreshItems){ $select.refreshItems(); } if($select.sizeSearchInput){ $select.sizeSearchInput(); } }; // Remove item from multiple select ctrl.removeChoice = function(index){ // if the choice is locked, don't remove it if($select.isLocked(null, index)) return false; var removedChoice = $select.selected[index]; 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.updateModel(); return true; }; ctrl.getPlaceholder = function(){ //Refactor single? if($select.selected && $select.selected.length) return; return $select.placeholder; }; }], controllerAs: '$selectMultiple', link: function(scope, element, attrs, ctrls) { var $select = ctrls[0]; var ngModel = scope.ngModel = ctrls[1]; var $selectMultiple = scope.$selectMultiple; //$select.selected = raw selected objects (ignoring any property binding) $select.multiple = true; //Input that will handle focus $select.focusInput = $select.searchInput; //Properly check for empty if set to multiple ngModel.$isEmpty = function(value) { return !value || value.length === 0; }; //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 && $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search locals = {}, result; if (!data) return inputValue; var resultMultiple = []; var checkFnMultiple = function(list, value){ if (!list || !list.length) return; 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 propsItemNameMatches = /(\w*)\./.exec($select.parserResult.trackByExp); var matches = /\.([^\s]+)/.exec($select.parserResult.trackByExp); if(propsItemNameMatches && propsItemNameMatches.length > 0 && propsItemNameMatches[1] == $select.parserResult.itemName){ if(matches && matches.length>0 && result[matches[1]] == value[matches[1]]){ resultMultiple.unshift(list[p]); return true; } } } if (angular.equals(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--) { //Check model array of currently selected items if (!checkFnMultiple($select.selected, inputValue[k])){ //Check model array of all items available if (!checkFnMultiple(data, inputValue[k])){ //If not found on previous lists, just add it directly to resultMultiple resultMultiple.unshift(inputValue[k]); } } } return resultMultiple; }); //Watch for external model changes scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) { if (oldValue != newValue){ //update the view value with fresh data from items, if there is a valid model value if(angular.isDefined(ngModel.$modelValue)) { ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters } $selectMultiple.refreshComponent(); } }); ngModel.$render = function() { // Make sure that model value is array if(!angular.isArray(ngModel.$viewValue)){ // Have tolerance for null or undefined values if (isNil(ngModel.$viewValue)){ ngModel.$viewValue = []; } else { throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue); } } $select.selected = ngModel.$viewValue; $selectMultiple.refreshComponent(); scope.$evalAsync(); //To force $digest }; scope.$on('uis:select', function (event, item) { if($select.selected.length >= $select.limit) { return; } $select.selected.push(item); var locals = {}; locals[$select.parserResult.itemName] = item; $timeout(function(){ $select.onSelectCallback(scope, { $item: item, $model: $select.parserResult.modelMapper(scope, locals) }); }); $selectMultiple.updateModel(); }); 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){ if($selectMultiple.removeChoice(curr)) { return prev; } else { return curr; } } else { // If nothing yet selected, select last item 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); // verify the new tag doesn't match the value of a possible selection choice or an already selected item. if ( stashArr.some(function (origItem) { return angular.equals(origItem, newItem); }) || $select.selected.some(function (origItem) { return angular.equals(origItem, newItem); }) ) { scope.$evalAsync(function () { $select.activeIndex = 0; $select.items = items; }); return; } if (newItem) 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 = []; if (newItem) items.push(newItem); items = items.concat(stashArr); } scope.$evalAsync( function () { $select.activeIndex = 0; $select.items = items; if ($select.isGrouped) { // update item references in groups, so that indexOf will work after angular.copy var itemsWithoutTag = newItem ? items.slice(1) : items; $select.setItemsFn(itemsWithoutTag); if (newItem) { // add tag item as a new group $select.items.unshift(newItem); $select.groups.unshift({name: '', items: [newItem], tagging: true}); } } }); } }); 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 <tempArr.length; i++) { // handle the simple string version of tagging if ( $select.tagging.fct === undefined ) { // search the array for the match if ( tempArr[i]+' '+$select.taggingLabel === needle ) { dupeIndex = i; } // handle the object tagging implementation } else { var mockObj = tempArr[i]; if (angular.isObject(mockObj)) { mockObj.isTag = true; } if ( angular.equals(mockObj, needle) ) { dupeIndex = i; } } } } return dupeIndex; } $select.searchInput.on('blur', function() { $timeout(function() { $selectMultiple.activeMatchIndex = -1; }); }); } }; }]);