diff --git a/karma.conf.js b/karma.conf.js
index af8c3539a..9a8c9fe82 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -15,6 +15,7 @@ module.exports = function(config) {
'bower_components/angular-mocks/angular-mocks.js',
'dist/select.js',
+ 'test/helpers.js',
'test/**/*.spec.js'
],
diff --git a/src/bootstrap/choices.tpl.html b/src/bootstrap/choices.tpl.html
index 472dd8549..db6ff026c 100644
--- a/src/bootstrap/choices.tpl.html
+++ b/src/bootstrap/choices.tpl.html
@@ -1,7 +1,11 @@
diff --git a/src/select.css b/src/select.css
index 5fec0482a..04c3991d6 100644
--- a/src/select.css
+++ b/src/select.css
@@ -95,6 +95,29 @@
overflow-x: hidden;
}
+.ui-select-bootstrap .ui-select-choices-row>a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: 400;
+ line-height: 1.42857143;
+ color: #333;
+ white-space: nowrap;
+}
+
+.ui-select-bootstrap .ui-select-choices-row>a:hover, .ui-select-bootstrap .ui-select-choices-row>a:focus {
+ text-decoration: none;
+ color: #262626;
+ background-color: #f5f5f5;
+}
+
+.ui-select-bootstrap .ui-select-choices-row.active>a {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ background-color: #428bca;
+}
+
/* fix hide/show angular animation */
.ui-select-match.ng-hide-add,
.ui-select-search.ng-hide-add {
diff --git a/src/select.js b/src/select.js
index 6e666f904..05fdc51b3 100644
--- a/src/select.js
+++ b/src/select.js
@@ -88,8 +88,12 @@
};
};
- self.getNgRepeatExpression = function(lhs, rhs, trackByExp) {
- var expression = lhs + ' in ' + rhs;
+ self.getGroupNgRepeatExpression = function() {
+ return '($group, $items) in $select.groups';
+ };
+
+ self.getNgRepeatExpression = function(lhs, rhs, trackByExp, grouped) {
+ var expression = lhs + ' in ' + (grouped ? '$items' : rhs);
if (trackByExp) {
expression += ' track by ' + trackByExp;
}
@@ -153,8 +157,33 @@
}
};
- ctrl.parseRepeatAttr = function(repeatAttr) {
- var repeat = RepeatParser.parse(repeatAttr);
+ ctrl.parseRepeatAttr = function(repeatAttr, groupByExp) {
+ function updateGroups(items) {
+ ctrl.groups = {};
+ angular.forEach(items, function(item) {
+ var groupFn = $scope.$eval(groupByExp);
+ var groupValue = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
+ if(!ctrl.groups[groupValue]) {
+ ctrl.groups[groupValue] = [item];
+ }
+ else {
+ ctrl.groups[groupValue].push(item);
+ }
+ });
+ ctrl.items = [];
+ angular.forEach(Object.keys(ctrl.groups).sort(), function(group) {
+ ctrl.items = ctrl.items.concat(ctrl.groups[group]);
+ });
+ }
+
+ function setPlainItems(items) {
+ ctrl.items = items;
+ }
+
+ var repeat = RepeatParser.parse(repeatAttr),
+ setItemsFn = groupByExp ? updateGroups : setPlainItems;
+
+ ctrl.itemProperty = repeat.lhs;
// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
$scope.$watchCollection(repeat.rhs, function(items) {
@@ -169,7 +198,7 @@
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
// Regular case
- ctrl.items = items;
+ setItemsFn(items);
}
}
@@ -198,6 +227,14 @@
}
};
+ ctrl.setActiveItem = function(item) {
+ ctrl.activeIndex = ctrl.items.indexOf(item);
+ };
+
+ ctrl.isActive = function(itemScope) {
+ return ctrl.items.indexOf(itemScope[ctrl.itemProperty]) === ctrl.activeIndex;
+ };
+
// When the user clicks on an item inside the dropdown
ctrl.select = function(item) {
ctrl.selected = item;
@@ -493,15 +530,21 @@
compile: function(tElement, tAttrs) {
var repeat = RepeatParser.parse(tAttrs.repeat);
+ var groupByExp = tAttrs.groupBy;
return function link(scope, element, attrs, $select, transcludeFn) {
-
- var rows = element.querySelectorAll('.ui-select-choices-row');
- if (rows.length !== 1) {
- throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", rows.length);
+
+ if(groupByExp) {
+ var groups = element.querySelectorAll('.ui-select-choices-group');
+ groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
+ }
+
+ var choices = element.querySelectorAll('.ui-select-choices-row');
+ if (choices.length !== 1) {
+ throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
}
- rows.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp))
- .attr('ng-mouseenter', '$select.activeIndex = $index')
+ choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp, groupByExp))
+ .attr('ng-mouseenter', '$select.setActiveItem('+repeat.lhs+')')
.attr('ng-click', '$select.select(' + repeat.lhs + ')');
@@ -509,12 +552,12 @@
var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner');
if (rowsInner.length !== 1)
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length);
-
+
rowsInner.append(clone);
$compile(element)(scope);
});
- $select.parseRepeatAttr(attrs.repeat);
+ $select.parseRepeatAttr(attrs.repeat, groupByExp);
scope.$watch('$select.search', function() {
$select.activeIndex = 0;
@@ -565,4 +608,4 @@
return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '
$&') : matchItem;
};
});
-}());
\ No newline at end of file
+}());
diff --git a/src/select2/choices.tpl.html b/src/select2/choices.tpl.html
index 82ddea791..0ded38010 100644
--- a/src/select2/choices.tpl.html
+++ b/src/select2/choices.tpl.html
@@ -1,5 +1,10 @@
diff --git a/src/selectize/choices.tpl.html b/src/selectize/choices.tpl.html
index 58a254f4c..244c16009 100644
--- a/src/selectize/choices.tpl.html
+++ b/src/selectize/choices.tpl.html
@@ -1,8 +1,10 @@
diff --git a/test/helpers.js b/test/helpers.js
new file mode 100644
index 000000000..9544675a7
--- /dev/null
+++ b/test/helpers.js
@@ -0,0 +1,15 @@
+beforeEach(function() {
+ jasmine.addMatchers({
+ toHaveClass: function(util, customEqualityTesters) {
+ return {
+ compare: function(actual, cls) {
+ var pass = actual.hasClass(cls);
+ return {
+ pass: pass,
+ message: "Expected '" + actual + "'" + (pass ? ' not ' : ' ') + "to have class '" + cls + "'."
+ }
+ }
+ }
+ }
+ });
+});
diff --git a/test/select.spec.js b/test/select.spec.js
index c502e9de7..a9709ab0c 100644
--- a/test/select.spec.js
+++ b/test/select.spec.js
@@ -9,15 +9,19 @@ describe('ui-select tests', function() {
scope = $rootScope.$new();
$compile = _$compile_;
+ scope.getGroupLabel = function(person) {
+ return person.age % 2 ? 'even' : 'odd';
+ };
+
scope.people = [
- { name: 'Adam', email: 'adam@email.com', age: 10 },
- { name: 'Amalie', email: 'amalie@email.com', age: 12 },
- { name: 'Wladimir', email: 'wladimir@email.com', age: 30 },
- { name: 'Samantha', email: 'samantha@email.com', age: 31 },
- { name: 'Estefanía', email: 'estefanía@email.com', age: 16 },
- { name: 'Natasha', email: 'natasha@email.com', age: 54 },
- { name: 'Nicole', email: 'nicole@email.com', age: 43 },
- { name: 'Adrian', email: 'adrian@email.com', age: 21 }
+ { name: 'Adam', email: 'adam@email.com', group: 'Foo', age: 12 },
+ { name: 'Amalie', email: 'amalie@email.com', group: 'Foo', age: 12 },
+ { name: 'Estefanía', email: 'estefanía@email.com', group: 'Foo', age: 21 },
+ { name: 'Adrian', email: 'adrian@email.com', group: 'Foo', age: 21 },
+ { name: 'Wladimir', email: 'wladimir@email.com', group: 'Foo', age: 30 },
+ { name: 'Samantha', email: 'samantha@email.com', group: 'bar', age: 30 },
+ { name: 'Nicole', email: 'nicole@email.com', group: 'bar', age: 43 },
+ { name: 'Natasha', email: 'natasha@email.com', group: 'Baz', age: 54 }
];
}));
@@ -69,6 +73,12 @@ describe('ui-select tests', function() {
return el.scope().$select.open && el.hasClass('open');
}
+ function triggerKeydown(element, keyCode) {
+ var e = jQuery.Event("keydown");
+ e.which = keyCode;
+ e.keyCode = keyCode;
+ element.trigger(e);
+ }
// Tests
@@ -183,6 +193,78 @@ describe('ui-select tests', function() {
expect(getMatchLabel(el)).toEqual('false');
});
+ describe('choices group', function() {
+ function getGroupLabel(item) {
+ return item.parent('.ui-select-choices-group').find('.ui-select-choices-group-label');
+ }
+ function createUiSelect() {
+ return compileTemplate(
+ '
\
+ {{$select.selected.name}} \
+ \
+ \
+ \
+ \
+ '
+ );
+ }
+
+ it('should create items group', function() {
+ var el = createUiSelect();
+ expect(el.find('.ui-select-choices-group').length).toBe(3);
+ });
+
+ it('should show label before each group', function() {
+ var el = createUiSelect();
+ expect(el.find('.ui-select-choices-group .ui-select-choices-group-label').map(function() {
+ return this.textContent;
+ }).toArray()).toEqual(['Baz', 'Foo', 'bar']);
+ });
+
+ it('should hide empty groups', function() {
+ var el = createUiSelect();
+ el.scope().$select.search = 'd';
+ scope.$digest();
+
+ expect(el.find('.ui-select-choices-group .ui-select-choices-group-label').map(function() {
+ return this.textContent;
+ }).toArray()).toEqual(['Foo']);
+ });
+
+ it('should change activeItem through groups', function() {
+ var el = createUiSelect();
+ el.scope().$select.search = 'n';
+ scope.$digest();
+ var choices = el.find('.ui-select-choices-row');
+ expect(choices.eq(0)).toHaveClass('active');
+ expect(getGroupLabel(choices.eq(0)).text()).toBe('Baz');
+
+ triggerKeydown(el.find('input'), 40 /*Down*/);
+ scope.$digest();
+ expect(choices.eq(1)).toHaveClass('active');
+ expect(getGroupLabel(choices.eq(1)).text()).toBe('Foo');
+ });
+ });
+
+ describe('choices group by function', function() {
+ function createUiSelect() {
+ return compileTemplate(
+ '
\
+ {{$select.selected.name}} \
+ \
+ \
+ \
+ '
+ );
+ }
+ it("should extract group value through function", function () {
+ var el = createUiSelect();
+ expect(el.find('.ui-select-choices-group .ui-select-choices-group-label').map(function() {
+ return this.textContent;
+ }).toArray()).toEqual(['even', 'odd']);
+ });
+ });
+
it('should throw when no ui-select-choices found', function() {
expect(function() {
compileTemplate(