Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

Allow sorting of multiple select #651

Merged
merged 2 commits into from
Feb 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/demo-multi-select.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ <h3>Array of strings</h3>
</ui-select>
<p>Selected: {{multipleDemo.colors}}</p>
<hr>
<h3>Array of objects</h3>
<ui-select multiple ng-model="multipleDemo.selectedPeople" theme="bootstrap" ng-disabled="disabled" close-on-select="false" style="width: 800px;">
<h3>Array of objects (sorting enabled)</h3>
<ui-select multiple ng-model="multipleDemo.selectedPeople" theme="bootstrap" ng-disabled="disabled" sortable="true" close-on-select="false" style="width: 800px;">
<ui-select-match placeholder="Select person...">{{$item.name}} &lt;{{$item.email}}&gt;</ui-select-match>
<ui-select-choices repeat="person in people | propsFilter: {name: $select.search, age: $select.search}">
<div ng-bind-html="person.name | highlight: $select.search"></div>
Expand Down
4 changes: 2 additions & 2 deletions examples/demo-tagging.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
<h1>Tagging Demos</h1>

<h3>Simple String Tags</h3>
<h4>(With Custom Tag Label)</h4>
<ui-select multiple tagging tagging-label="(custom 'new' label)" ng-model="multipleDemo.colors" theme="bootstrap" ng-disabled="disabled" style="width: 300px;" title="Choose a color">
<h4>(With Custom Tag Label / Sort Enabled)</h4>
<ui-select multiple tagging tagging-label="(custom 'new' label)" ng-model="multipleDemo.colors" theme="bootstrap" sortable="true" ng-disabled="disabled" style="width: 300px;" title="Choose a color">
<ui-select-match placeholder="Select colors...">{{$item}}</ui-select-match>
<ui-select-choices repeat="color in availableColors | filter:$select.search">
{{color}}
Expand Down
15 changes: 15 additions & 0 deletions examples/select2-bootstrap3.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@
</div>
</div>

<div class="form-group">
<label class="col-sm-3 control-label">Multiple</label>
<div class="col-sm-6">

<ui-select multiple sortable="true" ng-model="person.selected" theme="select2" class="form-control" title="Choose a person">
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="item in people | filter: $select.search">
<div ng-bind-html="item.name | highlight: $select.search"></div>
<small ng-bind-html="item.email | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>

</div>
</div>

<div class="form-group">
<label class="col-sm-3 control-label">Grouped</label>
<div class="col-sm-6">
Expand Down
4 changes: 2 additions & 2 deletions src/bootstrap/match-multiple.tpl.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<span class="ui-select-match">
<span ng-repeat="$item in $select.selected">
<span
style="margin-right: 3px;"
class="ui-select-match-item btn btn-default btn-xs"
tabindex="-1"
type="button"
ng-disabled="$select.disabled"
ng-click="$select.activeMatchIndex = $index;"
ng-class="{'btn-primary':$select.activeMatchIndex === $index, 'select-locked':$select.isLocked(this, $index)}">
ng-class="{'btn-primary':$select.activeMatchIndex === $index, 'select-locked':$select.isLocked(this, $index)}"
ui-select-sort="$select.selected">
<span class="close ui-select-match-close" ng-hide="$select.disabled" ng-click="$select.removeChoice($index)">&nbsp;&times;</span>
<span uis-transclude-append></span>
</span>
Expand Down
3 changes: 2 additions & 1 deletion src/bootstrap/select-multiple.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
ng-click="$select.activate()"
ng-model="$select.search"
role="combobox"
aria-label="{{ $select.baseTitle }}">
aria-label="{{ $select.baseTitle }}"
ondrop="return false;">
</div>
<div class="ui-select-choices"></div>
</div>
31 changes: 29 additions & 2 deletions src/select.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,54 @@
height: auto;
max-height: 200px;
overflow-x: hidden;
margin-top: -1px;
}

.ui-select-multiple.ui-select-bootstrap {
height: auto;
padding: .3em;
padding: 3px 3px 0 3px;
}

.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;
margin-bottom: 3px;
}

.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {
font-size: 1.6em;
line-height: 0.75;
}

.ui-select-multiple.ui-select-bootstrap .ui-select-match-item{
.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {
outline: 0;
margin: 0 3px 3px 0;
}

.ui-select-multiple .ui-select-match-item {
position: relative;
}

.ui-select-multiple .ui-select-match-item.dropping-before:before {
content: "";
position: absolute;
top: 0;
right: 100%;
height: 100%;
margin-right: 2px;
border-left: 1px solid #428bca;
}

.ui-select-multiple .ui-select-match-item.dropping-after:after {
content: "";
position: absolute;
top: 0;
left: 100%;
height: 100%;
margin-left: 2px;
border-right: 1px solid #428bca;
}

.ui-select-bootstrap .ui-select-choices-row>a {
Expand Down
141 changes: 141 additions & 0 deletions src/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
.constant('uiSelectConfig', {
theme: 'bootstrap',
searchEnabled: true,
sortable: false,
placeholder: '', // Empty by default, like HTML tag <select>
refreshDelay: 1000, // In milliseconds
closeOnSelect: true,
Expand Down Expand Up @@ -183,6 +184,7 @@
ctrl.focusser = undefined; //Reference to input element used to handle focus events
ctrl.disabled = undefined; // Initialized inside uiSelect directive link function
ctrl.searchEnabled = undefined; // Initialized inside uiSelect directive link function
ctrl.sortable = 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
Expand Down Expand Up @@ -1078,6 +1080,11 @@
$select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
});

scope.$watch('sortable', function() {
var sortable = scope.$eval(attrs.sortable);
$select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
});

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;
Expand Down Expand Up @@ -1317,6 +1324,140 @@
};
}])

// Make multiple matches sortable
.directive('uiSelectSort', ['$timeout', 'uiSelectConfig', 'uiSelectMinErr', function($timeout, uiSelectConfig, uiSelectMinErr) {
return {
require: '^uiSelect',
link: function(scope, element, attrs, $select) {
if (scope[attrs.uiSelectSort] === null) {
throw uiSelectMinErr('sort', "Expected a list to sort");
}

var options = angular.extend({
axis: 'horizontal'
},
scope.$eval(attrs.uiSelectSortOptions));

var axis = options.axis,
draggingClassName = 'dragging',
droppingClassName = 'dropping',
droppingBeforeClassName = 'dropping-before',
droppingAfterClassName = 'dropping-after';

scope.$watch(function(){
return $select.sortable;
}, function(n){
if (n) {
element.attr('draggable', true);
} else {
element.removeAttr('draggable');
}
});

element.on('dragstart', function(e) {
element.addClass(draggingClassName);

(e.dataTransfer || e.originalEvent.dataTransfer).setData('text/plain', scope.$index);
});

element.on('dragend', function() {
element.removeClass(draggingClassName);
});

var move = function(from, to) {
/*jshint validthis: true */
this.splice(to, 0, this.splice(from, 1)[0]);
};

var dragOverHandler = function(e) {
e.preventDefault();

var offset = axis === 'vertical' ? e.offsetY || e.layerY || (e.originalEvent ? e.originalEvent.offsetY : 0) : e.offsetX || e.layerX || (e.originalEvent ? e.originalEvent.offsetX : 0);

if (offset < (this[axis === 'vertical' ? 'offsetHeight' : 'offsetWidth'] / 2)) {
element.removeClass(droppingAfterClassName);
element.addClass(droppingBeforeClassName);

} else {
element.removeClass(droppingBeforeClassName);
element.addClass(droppingAfterClassName);
}
};

var dropTimeout;

var dropHandler = function(e) {
e.preventDefault();

var droppedItemIndex = parseInt((e.dataTransfer || e.originalEvent.dataTransfer).getData('text/plain'), 10);

// prevent event firing multiple times in firefox
$timeout.cancel(dropTimeout);
dropTimeout = $timeout(function() {
_dropHandler(droppedItemIndex);
}, 20);
};

var _dropHandler = function(droppedItemIndex) {
var theList = scope.$eval(attrs.uiSelectSort),
itemToMove = theList[droppedItemIndex],
newIndex = null;

if (element.hasClass(droppingBeforeClassName)) {
if (droppedItemIndex < scope.$index) {
newIndex = scope.$index - 1;
} else {
newIndex = scope.$index;
}
} else {
if (droppedItemIndex < scope.$index) {
newIndex = scope.$index;
} else {
newIndex = scope.$index + 1;
}
}

move.apply(theList, [droppedItemIndex, newIndex]);

scope.$apply(function() {
scope.$emit('uiSelectSort:change', {
array: theList,
item: itemToMove,
from: droppedItemIndex,
to: newIndex
});
});

element.removeClass(droppingClassName);
element.removeClass(droppingBeforeClassName);
element.removeClass(droppingAfterClassName);

element.off('drop', dropHandler);
};

element.on('dragenter', function() {
if (element.hasClass(draggingClassName)) {
return;
}

element.addClass(droppingClassName);

element.on('dragover', dragOverHandler);
element.on('drop', dropHandler);
});

element.on('dragleave', function() {
element.removeClass(droppingClassName);
element.removeClass(droppingBeforeClassName);
element.removeClass(droppingAfterClassName);

element.off('dragover', dragOverHandler);
element.off('drop', dropHandler);
});
}
};
}])

/**
* Highlights text that matches $select.search.
*
Expand Down
3 changes: 2 additions & 1 deletion src/select2/match-multiple.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
-->
<span class="ui-select-match">
<li class="ui-select-match-item select2-search-choice" ng-repeat="$item in $select.selected"
ng-class="{'select2-search-choice-focus':$select.activeMatchIndex === $index, 'select2-locked':$select.isLocked(this, $index)}">
ng-class="{'select2-search-choice-focus':$select.activeMatchIndex === $index, 'select2-locked':$select.isLocked(this, $index)}"
ui-select-sort="$select.selected">
<span uis-transclude-append></span>
<a href="javascript:;" class="ui-select-match-close select2-search-choice-close" ng-click="$select.removeChoice($index)" tabindex="-1"></a>
</li>
Expand Down
3 changes: 2 additions & 1 deletion src/select2/select-multiple.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
ng-hide="$select.disabled"
ng-model="$select.search"
ng-click="$select.activate()"
style="width: 34px;">
style="width: 34px;"
ondrop="return false;">
</li>
</ul>
<div class="select2-drop select2-with-searchbox select2-drop-active"
Expand Down