From 5fa3482ad3f094e5f779887c9abb6d4893b8eea3 Mon Sep 17 00:00:00 2001 From: just-boris Date: Tue, 8 Jul 2014 12:35:27 +0400 Subject: [PATCH 1/2] add choices grouping using 'group by' expression --- examples/bootstrap.html | 15 +++++ examples/demo.js | 10 ++-- examples/select2-bootstrap3.html | 15 +++++ examples/selectize-bootstrap3.html | 15 +++++ karma.conf.js | 1 + src/bootstrap/choices.tpl.html | 8 ++- src/select.css | 23 ++++++++ src/select.js | 68 +++++++++++++++++----- src/select2/choices.tpl.html | 9 ++- src/selectize/choices.tpl.html | 8 ++- test/helpers.js | 15 +++++ test/select.spec.js | 92 ++++++++++++++++++++++++++++-- 12 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 test/helpers.js diff --git a/examples/bootstrap.html b/examples/bootstrap.html index 940b62a68..6e421e6a3 100644 --- a/examples/bootstrap.html +++ b/examples/bootstrap.html @@ -57,6 +57,21 @@ +
+ +
+ + + {{$select.selected.name}} + + + + + + +
+
+
diff --git a/examples/demo.js b/examples/demo.js index 46756f011..b16ea4e1c 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -58,14 +58,14 @@ app.controller('DemoCtrl', function($scope, $http) { $scope.person = {}; $scope.people = [ - { name: 'Adam', email: 'adam@email.com', age: 10 }, + { name: 'Adam', email: 'adam@email.com', age: 12 }, { name: 'Amalie', email: 'amalie@email.com', age: 12 }, + { name: 'Estefanía', email: 'estefanía@email.com', age: 21 }, + { name: 'Adrian', email: 'adrian@email.com', age: 21 }, { 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: 'Samantha', email: 'samantha@email.com', age: 30 }, { name: 'Nicole', email: 'nicole@email.com', age: 43 }, - { name: 'Adrian', email: 'adrian@email.com', age: 21 } + { name: 'Natasha', email: 'natasha@email.com', age: 54 } ]; $scope.address = {}; diff --git a/examples/select2-bootstrap3.html b/examples/select2-bootstrap3.html index 556ebdbd3..3d2f3d043 100644 --- a/examples/select2-bootstrap3.html +++ b/examples/select2-bootstrap3.html @@ -64,6 +64,21 @@
+
+ +
+ + + {{$select.selected.name}} + + + + + + +
+
+
diff --git a/examples/selectize-bootstrap3.html b/examples/selectize-bootstrap3.html index 4381c45b9..c8f5e203d 100644 --- a/examples/selectize-bootstrap3.html +++ b/examples/selectize-bootstrap3.html @@ -79,6 +79,21 @@
+
+ +
+ + + {{$select.selected.name}} + + + + + + +
+
+
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..a0aba55d2 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,30 @@ } }; - 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); + } + }); + setPlainItems(items); + } + + 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 +195,7 @@ throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items); } else { // Regular case - ctrl.items = items; + setItemsFn(items); } } @@ -198,6 +224,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 +527,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); + + var groups = element.querySelectorAll('.ui-select-choices-group'); + if(groupByExp) { + 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 +549,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 +605,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 @@
    -
  • -
    +
  • +
    {{$group}}
    +
      +
    • +
      +
    • +
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 @@
-
-
+
+
{{$group}}
+
+
+
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..3b7414812 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: 'Adam', email: 'adam@email.com', age: 12 }, { name: 'Amalie', email: 'amalie@email.com', age: 12 }, + { name: 'Estefanía', email: 'estefanía@email.com', age: 21 }, + { name: 'Adrian', email: 'adrian@email.com', age: 21 }, { 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: 'Samantha', email: 'samantha@email.com', age: 30 }, { name: 'Nicole', email: 'nicole@email.com', age: 43 }, - { name: 'Adrian', email: 'adrian@email.com', age: 21 } + { name: 'Natasha', email: 'natasha@email.com', 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(5); + }); + + 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(['12', '21', '30', '43', '54']); + }); + + 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(['12', '21', '30']); + }); + + it('should change activeItem through groups', function() { + var el = createUiSelect(); + el.scope().$select.search = 'd'; + scope.$digest(); + var choices = el.find('.ui-select-choices-row'); + expect(choices.eq(0)).toHaveClass('active'); + expect(getGroupLabel(choices.eq(0)).text()).toBe('12'); + + triggerKeydown(el.find('input'), 40 /*Down*/); + scope.$digest(); + expect(choices.eq(1)).toHaveClass('active'); + expect(getGroupLabel(choices.eq(1)).text()).toBe('21'); + }); + }); + + 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( From 59d1d18fc00e2a002299adaff5121044afafc321 Mon Sep 17 00:00:00 2001 From: just-boris Date: Thu, 10 Jul 2014 13:25:17 +0400 Subject: [PATCH 2/2] fix bug in arrow navigation --- examples/bootstrap.html | 2 +- examples/demo.js | 16 ++++++++-------- examples/select2-bootstrap3.html | 2 +- examples/selectize-bootstrap3.html | 2 +- src/select.js | 11 +++++++---- test/select.spec.js | 30 +++++++++++++++--------------- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/examples/bootstrap.html b/examples/bootstrap.html index 6e421e6a3..c98f63a9a 100644 --- a/examples/bootstrap.html +++ b/examples/bootstrap.html @@ -63,7 +63,7 @@ {{$select.selected.name}} - + diff --git a/examples/demo.js b/examples/demo.js index b16ea4e1c..1319f4af7 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -58,14 +58,14 @@ app.controller('DemoCtrl', function($scope, $http) { $scope.person = {}; $scope.people = [ - { name: 'Adam', email: 'adam@email.com', age: 12 }, - { name: 'Amalie', email: 'amalie@email.com', age: 12 }, - { name: 'Estefanía', email: 'estefanía@email.com', age: 21 }, - { name: 'Adrian', email: 'adrian@email.com', age: 21 }, - { name: 'Wladimir', email: 'wladimir@email.com', age: 30 }, - { name: 'Samantha', email: 'samantha@email.com', age: 30 }, - { name: 'Nicole', email: 'nicole@email.com', age: 43 }, - { name: 'Natasha', email: 'natasha@email.com', age: 54 } + { 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 } ]; $scope.address = {}; diff --git a/examples/select2-bootstrap3.html b/examples/select2-bootstrap3.html index 3d2f3d043..85dfc07ba 100644 --- a/examples/select2-bootstrap3.html +++ b/examples/select2-bootstrap3.html @@ -70,7 +70,7 @@ {{$select.selected.name}} - + diff --git a/examples/selectize-bootstrap3.html b/examples/selectize-bootstrap3.html index c8f5e203d..a14d4cff8 100644 --- a/examples/selectize-bootstrap3.html +++ b/examples/selectize-bootstrap3.html @@ -85,7 +85,7 @@ {{$select.selected.name}} - + diff --git a/src/select.js b/src/select.js index a0aba55d2..05fdc51b3 100644 --- a/src/select.js +++ b/src/select.js @@ -89,7 +89,7 @@ }; self.getGroupNgRepeatExpression = function() { - return '($group, $items) in $select.groups' + return '($group, $items) in $select.groups'; }; self.getNgRepeatExpression = function(lhs, rhs, trackByExp, grouped) { @@ -170,7 +170,10 @@ ctrl.groups[groupValue].push(item); } }); - setPlainItems(items); + ctrl.items = []; + angular.forEach(Object.keys(ctrl.groups).sort(), function(group) { + ctrl.items = ctrl.items.concat(ctrl.groups[group]); + }); } function setPlainItems(items) { @@ -530,9 +533,9 @@ var groupByExp = tAttrs.groupBy; return function link(scope, element, attrs, $select, transcludeFn) { - var groups = element.querySelectorAll('.ui-select-choices-group'); if(groupByExp) { - groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression()) + var groups = element.querySelectorAll('.ui-select-choices-group'); + groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression()); } var choices = element.querySelectorAll('.ui-select-choices-row'); diff --git a/test/select.spec.js b/test/select.spec.js index 3b7414812..a9709ab0c 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -14,14 +14,14 @@ describe('ui-select tests', function() { }; scope.people = [ - { name: 'Adam', email: 'adam@email.com', age: 12 }, - { name: 'Amalie', email: 'amalie@email.com', age: 12 }, - { name: 'Estefanía', email: 'estefanía@email.com', age: 21 }, - { name: 'Adrian', email: 'adrian@email.com', age: 21 }, - { name: 'Wladimir', email: 'wladimir@email.com', age: 30 }, - { name: 'Samantha', email: 'samantha@email.com', age: 30 }, - { name: 'Nicole', email: 'nicole@email.com', age: 43 }, - { name: 'Natasha', email: 'natasha@email.com', age: 54 } + { 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 } ]; })); @@ -201,7 +201,7 @@ describe('ui-select tests', function() { return compileTemplate( ' \ {{$select.selected.name}} \ - \ + \
\
\
\ @@ -211,14 +211,14 @@ describe('ui-select tests', function() { it('should create items group', function() { var el = createUiSelect(); - expect(el.find('.ui-select-choices-group').length).toBe(5); + 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(['12', '21', '30', '43', '54']); + }).toArray()).toEqual(['Baz', 'Foo', 'bar']); }); it('should hide empty groups', function() { @@ -228,21 +228,21 @@ describe('ui-select tests', function() { expect(el.find('.ui-select-choices-group .ui-select-choices-group-label').map(function() { return this.textContent; - }).toArray()).toEqual(['12', '21', '30']); + }).toArray()).toEqual(['Foo']); }); it('should change activeItem through groups', function() { var el = createUiSelect(); - el.scope().$select.search = 'd'; + 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('12'); + 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('21'); + expect(getGroupLabel(choices.eq(1)).text()).toBe('Foo'); }); });