diff --git a/src/select.css b/src/select.css
index 415f5deca..616a0c1c4 100644
--- a/src/select.css
+++ b/src/select.css
@@ -88,17 +88,31 @@
right: 15px;
}
-.ui-select-bootstrap > .ui-select-choices {
- width: 100%;
-}
-
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
.ui-select-bootstrap > .ui-select-choices {
+ width: 100%;
height: auto;
max-height: 200px;
overflow-x: hidden;
}
+.ui-select-multiple.ui-select-bootstrap {
+ height: auto;
+ padding: .3em;
+}
+
+.ui-select-multiple.ui-select-bootstrap input.ui-select-search {
+ background-color: transparent !important; /* To prevent double background when disabled */
+ border: none;
+ outline: none;
+ height: 1.666666em;
+}
+
+.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {
+ font-size: 1.6em;
+ line-height: 0.75;
+}
+
.ui-select-bootstrap .ui-select-choices-row>a {
display: block;
padding: 3px 20px;
diff --git a/src/select.js b/src/select.js
index dd1c489c4..1c532b599 100644
--- a/src/select.js
+++ b/src/select.js
@@ -1,6 +1,49 @@
(function () {
"use strict";
+ var KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ isControl: function (e) {
+ var k = e.which;
+ switch (k) {
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+
+ if (e.metaKey) return true;
+
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ },
+ isVerticalMovement: function (k){
+ return ~[KEY.UP, KEY.DOWN].indexOf(k);
+ },
+ isHorizontalMovement: function (k){
+ return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k);
+ }
+ };
+
/**
* Add querySelectorAll() to jqLite.
*
@@ -102,6 +145,7 @@
ctrl.placeholder = undefined;
ctrl.search = EMPTY_SEARCH;
ctrl.activeIndex = 0;
+ ctrl.activeMatchIndex = -1;
ctrl.items = [];
ctrl.selected = undefined;
ctrl.open = false;
@@ -111,6 +155,7 @@
ctrl.searchEnabled = undefined; // Initialized inside uiSelect directive link function
ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function
ctrl.refreshDelay = undefined; // Initialized inside uiSelectChoices directive link function
+ ctrl.multiple = false; // Initialized inside uiSelect directive link function
ctrl.isEmpty = function() {
return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === '';
@@ -126,17 +171,20 @@
if (ctrl.resetSearchInput) {
ctrl.search = EMPTY_SEARCH;
//reset activeIndex
- if (ctrl.selected && ctrl.items.length) {
+ if (ctrl.selected && ctrl.items.length && !ctrl.multiple) {
ctrl.activeIndex = ctrl.items.indexOf(ctrl.selected);
}
}
}
// When the user clicks on ui-select, displays the dropdown list
- ctrl.activate = function(initSearchValue) {
- if (!ctrl.disabled) {
- _resetSearchInput();
+ ctrl.activate = function(initSearchValue, avoidReset) {
+ if (!ctrl.disabled && !ctrl.open) {
+ if(!avoidReset) _resetSearchInput();
ctrl.open = true;
+ ctrl.activeMatchIndex = -1;
+
+ ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
// Give it time to appear before focus
$timeout(function() {
@@ -188,8 +236,13 @@
if (!angular.isArray(items)) {
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
- // Regular case
- setItemsFn(items);
+ 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);
+ }
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
}
@@ -197,6 +250,16 @@
});
+ if (ctrl.multiple){
+ //Remove already selected items
+ $scope.$watchCollection('$select.selected', function(selectedItems){
+ if (!selectedItems) return;
+ var data = ctrl.parserResult.source($scope);
+ var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
+ setItemsFn(filteredItems);
+ });
+ }
+
};
var _refreshDelayPromise;
@@ -238,11 +301,15 @@
ctrl.onSelectCallback($scope, {
$item: item,
$model: ctrl.parserResult.modelMapper($scope, locals)
- });
+ });
- ctrl.selected = item;
+ if(ctrl.multiple){
+ ctrl.selected.push(item);
+ ctrl.sizeSearchInput();
+ } else {
+ ctrl.selected = item;
+ }
ctrl.close();
- // Using a watch instead of $scope.ngModel.$setViewValue(item)
};
// Closes the dropdown
@@ -256,28 +323,53 @@
}
};
- var Key = {
- Enter: 13,
- Tab: 9,
- Up: 38,
- Down: 40,
- Escape: 27
+ // Remove item from multiple select
+ ctrl.removeChoice = function(index){
+ ctrl.selected.splice(index, 1);
+ ctrl.activeMatchIndex = -1;
+ ctrl.sizeSearchInput();
};
- function _onKeydown(key) {
+ ctrl.getPlaceholder = function(){
+ //Refactor single?
+ if(ctrl.multiple && ctrl.selected.length) return;
+ return ctrl.placeholder;
+ };
+
+ ctrl.sizeSearchInput = function(){
+ var input = _searchInput[0],
+ container = _searchInput.parent().parent()[0];
+ _searchInput.css('width','10px');
+ $timeout(function(){
+ var newWidth = container.clientWidth - input.offsetLeft - 10;
+ if(newWidth < 50) newWidth = container.clientWidth;
+ _searchInput.css('width',newWidth+'px');
+ }, 0, false);
+ };
+
+ function _handleDropDownSelection(key) {
var processed = true;
switch (key) {
- case Key.Down:
- if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
+ case KEY.DOWN:
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ else if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
+ break;
+ case KEY.UP:
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ else if (ctrl.activeIndex > 0) { ctrl.activeIndex--; }
break;
- case Key.Up:
- if (ctrl.activeIndex > 0) { ctrl.activeIndex--; }
+ case KEY.TAB:
+ //TODO: Que hacemos en modo multiple?
+ if (!ctrl.multiple) ctrl.select(ctrl.items[ctrl.activeIndex]);
break;
- case Key.Tab:
- case Key.Enter:
- ctrl.select(ctrl.items[ctrl.activeIndex]);
+ case KEY.ENTER:
+ if(ctrl.open){
+ ctrl.select(ctrl.items[ctrl.activeIndex]);
+ } else {
+ ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ }
break;
- case Key.Escape:
+ case KEY.ESC:
ctrl.close();
break;
default:
@@ -286,30 +378,114 @@
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) {
- // Keyboard shortcuts are all about the items,
- // does not make sense (and will crash) if ctrl.items is empty
- if (ctrl.items && ctrl.items.length >= 0) {
- var key = e.which;
-
- $scope.$apply(function() {
- var processed = _onKeydown(key);
- if (processed && key != Key.Tab) {
- e.preventDefault();
- e.stopPropagation();
- }
- });
- switch (key) {
- case Key.Down:
- case Key.Up:
- _ensureHighlightVisible();
- break;
+ var key = e.which;
+
+ // if(~[KEY.ESC,KEY.TAB].indexOf(key)){
+ // //TODO: SEGURO?
+ // ctrl.close();
+ // }
+
+ $scope.$apply(function() {
+ var processed = false;
+
+ if(ctrl.multiple && KEY.isHorizontalMovement(key)){
+ processed = _handleMatchSelection(key);
}
+
+ if (!processed && ctrl.items.length > 0) {
+ processed = _handleDropDownSelection(key);
+ }
+
+ 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){
+ _ensureHighlightVisible();
}
+
+ });
+
+ _searchInput.on('blur', function() {
+ $timeout(function() {
+ ctrl.activeMatchIndex = -1;
+ ctrl.activeIndex = 0;
+ });
});
+ 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;
+ }
+
// See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
function _ensureHighlightVisible() {
var container = $element.querySelectorAll('.ui-select-choices-content');
@@ -333,7 +509,7 @@
}
$scope.$on('$destroy', function() {
- _searchInput.off('keydown');
+ _searchInput.off('keydown blur');
});
}])
@@ -345,7 +521,7 @@
restrict: 'EA',
templateUrl: function(tElement, tAttrs) {
var theme = tAttrs.theme || uiSelectConfig.theme;
- return theme + '/select.tpl.html';
+ return theme + (angular.isDefined(tAttrs.multiple) ? '/select-multiple.tpl.html' : '/select.tpl.html');
},
replace: true,
transclude: true,
@@ -359,26 +535,70 @@
var $select = ctrls[0];
var ngModel = ctrls[1];
+ $select.multiple = angular.isDefined(attrs.multiple);
+
$select.onSelectCallback = $parse(attrs.onSelect);
//From view --> model
ngModel.$parsers.unshift(function (inputValue) {
- var locals = {};
- locals[$select.parserResult.itemName] = inputValue;
- var result = $select.parserResult.modelMapper(scope, locals);
- return result;
+ var locals = {},
+ result;
+ if ($select.multiple){
+ var resultMultiple = [];
+ for (var j = inputValue.length - 1; j >= 0; j--) {
+ locals = {};
+ locals[$select.parserResult.itemName] = inputValue[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);
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
+ locals = {},
+ result;
if (data){
- for (var i = data.length - 1; i >= 0; i--) {
- var locals = {};
- locals[$select.parserResult.itemName] = data[i];
- var result = $select.parserResult.modelMapper(scope, locals);
- if (result == inputValue){
- return data[i];
+ if ($select.multiple){
+ 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 (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];
}
}
}
@@ -393,100 +613,56 @@
$compile(focusser)(scope);
$select.focusser = focusser;
- element.append(focusser);
- focusser.bind("focus", function(){
- scope.$evalAsync(function(){
- $select.focus = true;
+ if (!$select.multiple){
+
+ element.append(focusser);
+ focusser.bind("focus", function(){
+ scope.$evalAsync(function(){
+ $select.focus = true;
+ });
});
- });
- focusser.bind("blur", function(){
- scope.$evalAsync(function(){
- $select.focus = false;
+ 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.$digest();
- return;
- }
+ focusser.bind("keydown", function(e){
+
+ if (e.which === KEY.BACKSPACE) {
+ e.preventDefault();
+ e.stopPropagation();
+ $select.select(undefined);
+ scope.$digest();
+ return;
+ }
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
- 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();
- }
+ 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();
- });
+ scope.$digest();
+ });
- focusser.bind("keyup input", function(e){
+ 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 regualar key, so we pass it to the search input
- focusser.val('');
- scope.$digest();
-
- });
+ 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();
- //TODO Refactor to reuse the KEY object from uiSelectCtrl
- var KEY = {
- TAB: 9,
- ENTER: 13,
- ESC: 27,
- SPACE: 32,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40,
- SHIFT: 16,
- CTRL: 17,
- ALT: 18,
- PAGE_UP: 33,
- PAGE_DOWN: 34,
- HOME: 36,
- END: 35,
- BACKSPACE: 8,
- DELETE: 46,
- isArrow: function (k) {
- k = k.which ? k.which : k;
- switch (k) {
- case KEY.LEFT:
- case KEY.RIGHT:
- case KEY.UP:
- case KEY.DOWN:
- return true;
- }
- return false;
- },
- isControl: function (e) {
- var k = e.which;
- switch (k) {
- case KEY.SHIFT:
- case KEY.CTRL:
- case KEY.ALT:
- return true;
- }
+ });
- if (e.metaKey) return true;
+ }
- return false;
- },
- isFunctionKey: function (k) {
- k = k.which ? k.which : k;
- return k >= 112 && k <= 123;
- }
- };
scope.$watch('searchEnabled', function() {
var searchEnabled = scope.$eval(attrs.searchEnabled);
@@ -504,13 +680,33 @@
$select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
});
- scope.$watch('$select.selected', function(newValue) {
- if (ngModel.$viewValue !== newValue) {
- ngModel.$setViewValue(newValue);
- }
- });
+ if ($select.multiple){
+ scope.$watchCollection('$select.selected', function(newValue) {
+ //On v1.2.19 the 2nd and 3rd parameteres are ignored
+ //On v1.3.0-beta+ 3rd parameter (revalidate) is true, to force $parsers to recreate model
+ ngModel.$setViewValue(newValue, null, true);
+ });
+ 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 = [];
+ } else {
+ throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
+ }
+ }
+ }
$select.selected = ngModel.$viewValue;
};
@@ -612,7 +808,8 @@
$compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend
- scope.$watch('$select.search', function() {
+ scope.$watch('$select.search', function(newValue) {
+ if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
$select.activeIndex = 0;
$select.refresh(attrs.refresh);
});
@@ -626,6 +823,7 @@
}
};
}])
+ // Recreates old behavior of ng-transclude. Used internally.
.directive('uisTranscludeAppend', function () {
return {
link: function (scope, element, attrs, ctrl, transclude) {
@@ -644,12 +842,18 @@
templateUrl: function(tElement) {
// Gets theme attribute from parent (ui-select)
var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
- return theme + '/match.tpl.html';
+ var multi = tElement.parent().attr('multiple');
+ return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html');
},
link: function(scope, element, attrs, $select) {
attrs.$observe('placeholder', function(placeholder) {
$select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
});
+
+ if($select.multiple){
+ $select.sizeSearchInput();
+ }
+
}
};
}])
diff --git a/src/select2/match-multiple.tpl.html b/src/select2/match-multiple.tpl.html
new file mode 100644
index 000000000..d4a2217e7
--- /dev/null
+++ b/src/select2/match-multiple.tpl.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/select2/select-multiple.tpl.html b/src/select2/select-multiple.tpl.html
new file mode 100644
index 000000000..b014aff99
--- /dev/null
+++ b/src/select2/select-multiple.tpl.html
@@ -0,0 +1,26 @@
+