Skip to content

Commit 18b153d

Browse files
committed
Added multiple select functionality to new dc.selectMenu
1 parent a73dda5 commit 18b153d

File tree

2 files changed

+133
-33
lines changed

2 files changed

+133
-33
lines changed

spec/select-menu-spec.js

+74-24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ describe('dc.selectMenu', function() {
3737
it('creates select tag', function() {
3838
expect(chart.selectAll("select").length).toEqual(1);
3939
});
40+
it('select tag is not a multiple select by default', function() {
41+
expect(chart.selectAll("select").attr("multiple")).toBeNull();
42+
});
43+
it('can be made into a multiple', function() {
44+
chart.multiple(true).redraw();
45+
expect(chart.selectAll("select").attr("multiple")).toBeTruthy();
46+
});
4047
it('creates prompt option with empty value', function() {
4148
var option = chart.selectAll("option")[0][0];
4249
expect(option).toBeTruthy();
@@ -76,36 +83,82 @@ describe('dc.selectMenu', function() {
7683
last_option = getOption(chart,last_index);
7784
expect(last_option.text).toEqual("Mississippi: 2");
7885
});
79-
})
86+
});
87+
88+
describe('regular single select', function() {
89+
describe('selecting an option', function () {
90+
it('filters dimension based on selected option\'s value', function(){
91+
chart.onChange(stateGroup.all()[0].key);
92+
expect(chart.filter()).toEqual("California");
93+
});
94+
it('replaces filter on second selection', function(){
95+
chart.onChange(stateGroup.all()[0].key);
96+
chart.onChange(stateGroup.all()[1].key);
97+
expect(chart.filter()).toEqual("Colorado");
98+
expect(chart.filters().length).toEqual(1);
99+
});
100+
it('actually filters dimension', function(){
101+
chart.onChange(stateGroup.all()[0].key);
102+
expect(regionGroup.all()[0].value).toEqual(0);
103+
expect(regionGroup.all()[3].value).toEqual(2);
104+
});
105+
it('removes filter when prompt option is selected', function(){
106+
chart.onChange(null);
107+
expect(chart.hasFilter()).not.toBeTruthy();
108+
expect(regionGroup.all()[0].value).toEqual(1);
109+
});
110+
});
111+
112+
describe('redraw with existing filter', function () {
113+
it('selects option corresponding to active filter', function(){
114+
chart.onChange(stateGroup.all()[0].key);
115+
chart.redraw();
116+
expect(chart.selectAll("select")[0][0].value).toEqual("California");
117+
});
118+
});
119+
120+
afterEach(function () {
121+
chart.onChange(null);
122+
});
123+
});
80124

81-
describe('selecting an option', function () {
82-
it('filters dimension based on selected option\'s value', function(){
83-
chart.onChange(stateGroup.all()[0].key);
84-
expect(chart.filter()).toEqual("California");
125+
describe('multiple select', function () {
126+
beforeEach(function () {
127+
chart.multiple(true);
128+
chart.onChange([stateGroup.all()[0].key, stateGroup.all()[1].key]);
85129
});
86-
it('replaces filter on second selection', function(){
87-
chart.onChange(stateGroup.all()[0].key);
88-
chart.onChange(stateGroup.all()[1].key);
89-
expect(chart.filter()).toEqual("Colorado");
90-
expect(chart.filters().length).toEqual(1);
130+
it('adds filters based on selections', function(){
131+
expect(chart.filters()).toEqual(["California", "Colorado"]);
132+
expect(chart.filters().length).toEqual(2);
91133
});
92134
it('actually filters dimension', function(){
93-
chart.onChange(stateGroup.all()[0].key);
94-
expect(regionGroup.all()[0].value).toEqual(0);
95135
expect(regionGroup.all()[3].value).toEqual(2);
136+
expect(regionGroup.all()[4].value).toEqual(2);
96137
});
97-
it('removes filter when prompt option is selected', function(){
98-
chart.onChange('');
138+
it('removes all filters when prompt option is selected', function(){
139+
chart.onChange(null);
99140
expect(chart.hasFilter()).not.toBeTruthy();
100141
expect(regionGroup.all()[0].value).toEqual(1);
101142
});
102-
});
143+
it('selects all options corresponding to active filters on redraw', function(){
144+
var selectedOptions = chart.selectAll("select").selectAll("option")[0].filter(function(d) {
145+
return d.selected;
146+
});
147+
expect(selectedOptions.length).toEqual(2);
148+
expect(selectedOptions.map(function(d){ return d.value; })).toEqual(["California", "Colorado"]);
149+
});
150+
it('does not deselect previously filtered options when new option is added', function(){
151+
chart.onChange([stateGroup.all()[0].key, stateGroup.all()[1].key, stateGroup.all()[5].key]);
103152

104-
describe('redraw with existing filter', function () {
105-
it('selects option corresponding to active filter', function(){
106-
chart.onChange(stateGroup.all()[0].key);
107-
chart.redraw();
108-
expect(chart.selectAll("select")[0][0].value).toEqual("California");
153+
var selectedOptions = chart.selectAll("select").selectAll("option")[0].filter(function(d) {
154+
return d.selected;
155+
});
156+
expect(selectedOptions.length).toEqual(3);
157+
expect(selectedOptions.map(function(d){ return d.value; })).toEqual(["California", "Colorado", "Ontario"]);
158+
});
159+
160+
afterEach(function () {
161+
chart.onChange(null);
109162
});
110163
});
111164

@@ -136,7 +189,4 @@ describe('dc.selectMenu', function() {
136189
function getOption(chart, i){
137190
return chart.selectAll("option.dc-select-option")[0][i];
138191
}
139-
140-
141-
});
142-
192+
});

src/select-menu.js

+59-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
## Select Menu
33
Includes: [Base Mixin](#base-mixin)
44
5-
The select menu is a simple widget that allows filtering a dimension by selecting an option from an HTML <select/> menu.
5+
The select menu is a simple widget designed to filter a dimension by selecting an option from
6+
an HTML <select/> menu. The menu can be optionally turned into a multiselect.
67
78
#### dc.selectMenu(parent[, chartGroup])
89
Create a select menu instance and attach it to the given parent element.
@@ -23,7 +24,7 @@ The select menu is a simple widget that allows filtering a dimension by selectin
2324
.dimension(states)
2425
.group(stateGroup);
2526
26-
// the option text can be set via the title function
27+
// the option text can be set via the title() function
2728
// by default the option text is '`key`: `value`'
2829
select.title(function(d){
2930
return 'STATE: ' + d.key;
@@ -39,6 +40,7 @@ dc.selectMenu = function (parent, chartGroup) {
3940

4041
var _select;
4142
var _promptText = 'Select all';
43+
var _multiple = false;
4244
var _order = function (a, b) {
4345
return _chart.keyAccessor()(a) > _chart.keyAccessor()(b) ?
4446
1 : _chart.keyAccessor()(b) > _chart.keyAccessor()(a) ?
@@ -55,16 +57,27 @@ dc.selectMenu = function (parent, chartGroup) {
5557

5658
_chart._doRender = function () {
5759
_chart.select('select').remove();
58-
_select = _chart.root().append('select').classed(SELECT_CSS_CLASS, true);
60+
_select = _chart.root().append('select')
61+
.classed(SELECT_CSS_CLASS, true);
62+
63+
switchMultipleSelectOption();
64+
5965
_select.append('option').text(_promptText).attr('value', '');
6066
renderOptions();
6167
return _chart;
6268
};
6369

6470
_chart._doRedraw = function () {
71+
switchMultipleSelectOption();
6572
renderOptions();
66-
// select the option that corresponds to current filter
67-
if (_chart.hasFilter()) {
73+
// select the option(s) corresponding to current filter(s)
74+
if (_chart.hasFilter() && _multiple) {
75+
_select.selectAll('option')
76+
.filter(function (d) {
77+
return d && _chart.filters().indexOf(String(_chart.keyAccessor()(d))) >= 0;
78+
})
79+
.property('selected', true);
80+
} else if (_chart.hasFilter()) {
6881
_select.property('value', _chart.filter());
6982
} else {
7083
_select.property('value', '');
@@ -89,12 +102,24 @@ dc.selectMenu = function (parent, chartGroup) {
89102
return options;
90103
}
91104

92-
function onChange () {
93-
_chart.onChange(this.value);
105+
function onChange (d , i) {
106+
var selectedOptions = Array.prototype.slice.call(d3.event.target.selectedOptions);
107+
var values = selectedOptions.map(function (d) {
108+
return d.value;
109+
});
110+
// check if only prompt option is selected
111+
if (values.length === 1 && values[0] === '') {
112+
values = null;
113+
} else if (values.length === 1) {
114+
values = values[0];
115+
}
116+
_chart.onChange(values);
94117
}
95118

96119
_chart.onChange = function (val) {
97-
if (val) {
120+
if (val && _multiple) {
121+
_chart.replaceFilter([val]);
122+
} else if (val) {
98123
_chart.replaceFilter(val);
99124
} else {
100125
_chart.filterAll();
@@ -104,6 +129,14 @@ dc.selectMenu = function (parent, chartGroup) {
104129
});
105130
};
106131

132+
function switchMultipleSelectOption () {
133+
if (_multiple) {
134+
_select.attr('multiple', true);
135+
} else {
136+
_select.attr('multiple', null);
137+
}
138+
}
139+
107140
/**
108141
#### .order([function])
109142
Get or set the function that controls the ordering of option tags in the
@@ -159,5 +192,22 @@ dc.selectMenu = function (parent, chartGroup) {
159192
return _chart;
160193
};
161194

195+
/**
196+
#### .multiple([bool])
197+
Controls the type of select menu (single select is default). Setting it to true converts the underlying
198+
HTML tag into a multiple select.
199+
```
200+
chart.multiple(true);
201+
```
202+
**/
203+
_chart.multiple = function (_) {
204+
if (!arguments.length) {
205+
return _multiple;
206+
}
207+
_multiple = _;
208+
209+
return _chart;
210+
};
211+
162212
return _chart.anchor(parent, chartGroup);
163-
};
213+
};

0 commit comments

Comments
 (0)