From 9994f681140fd0614ec6b7fcf6afdca4ae78e005 Mon Sep 17 00:00:00 2001 From: Wladimir Coka Date: Sun, 8 Mar 2015 20:13:49 -0500 Subject: [PATCH] chore(refactor): split code into multiple directives --- src/bootstrap/match-multiple.tpl.html | 6 +- src/bootstrap/select-multiple.tpl.html | 2 +- src/common.css | 4 +- src/select2/match-multiple.tpl.html | 4 +- src/select2/select-multiple.tpl.html | 2 +- src/uiSelectChoicesDirective.js | 2 +- src/uiSelectController.js | 388 ++----------- src/uiSelectDirective.js | 546 ++++++------------ src/uiSelectMultipleDirective.js | 393 +++++++++++++ src/uiSelectSingleDirective.js | 124 ++++ ...erService.js => uisRepeatParserService.js} | 2 +- test/select.spec.js | 24 +- 12 files changed, 785 insertions(+), 712 deletions(-) create mode 100644 src/uiSelectMultipleDirective.js create mode 100644 src/uiSelectSingleDirective.js rename src/{RepeatParserService.js => uisRepeatParserService.js} (94%) diff --git a/src/bootstrap/match-multiple.tpl.html b/src/bootstrap/match-multiple.tpl.html index 3e60ccd0d..dffcf3b6e 100644 --- a/src/bootstrap/match-multiple.tpl.html +++ b/src/bootstrap/match-multiple.tpl.html @@ -5,10 +5,10 @@ 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)}" + ng-click="$selectMultiple.activeMatchIndex = $index;" + ng-class="{'btn-primary':$selectMultiple.activeMatchIndex === $index, 'select-locked':$select.isLocked(this, $index)}" ui-select-sort="$select.selected"> -  × +  × diff --git a/src/bootstrap/select-multiple.tpl.html b/src/bootstrap/select-multiple.tpl.html index 26f2c7dbf..1628c9461 100644 --- a/src/bootstrap/select-multiple.tpl.html +++ b/src/bootstrap/select-multiple.tpl.html @@ -7,7 +7,7 @@ autocapitalize="off" spellcheck="false" class="ui-select-search input-xs" - placeholder="{{$select.getPlaceholder()}}" + placeholder="{{$selectMultiple.getPlaceholder()}}" ng-disabled="$select.disabled" ng-hide="$select.disabled" ng-click="$select.activate()" diff --git a/src/common.css b/src/common.css index 6939207ad..d51edfb85 100644 --- a/src/common.css +++ b/src/common.css @@ -100,9 +100,9 @@ body > .select2-container { border-bottom-right-radius: 0; } -.ui-select-bootstrap > .ui-select-match { +.ui-select-bootstrap > .ui-select-match > .btn{ /* Instead of center because of .btn */ - text-align: left; + text-align: left !important; } .ui-select-bootstrap > .ui-select-match > .caret { diff --git a/src/select2/match-multiple.tpl.html b/src/select2/match-multiple.tpl.html index b18f54933..f107ac18c 100644 --- a/src/select2/match-multiple.tpl.html +++ b/src/select2/match-multiple.tpl.html @@ -5,9 +5,9 @@ -->
  • - +
  • 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); });