import {events} from '../core/events';
import {BaseMixin} from '../base/base-mixin';
import {logger} from '../core/logger';
import {d3compat} from '../core/config';
const SELECT_CSS_CLASS = 'dc-select-menu';
const OPTION_CSS_CLASS = 'dc-select-option';
/**
* The select menu is a simple widget designed to filter a dimension by selecting an option from
* an HTML `<select/>` menu. The menu can be optionally turned into a multiselect.
* @mixes BaseMixin
*/
export class SelectMenu extends BaseMixin {
/**
* Create a Select Menu.
* @example
* // create a select menu under #select-container using the default global chart group
* var select = new SelectMenu('#select-container')
* .dimension(states)
* .group(stateGroup);
* // the option text can be set via the title() function
* // by default the option text is '`key`: `value`'
* select.title(function (d){
* return 'STATE: ' + d.key;
* })
* @param {String|node|d3.selection|CompositeChart} parent - Any valid
* [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying
* a dom block element such as a div; or a dom element or d3 selection.
* @param {String} [chartGroup] - The name of the chart group this widget should be placed in.
* Interaction with the widget will only trigger events and redraws within its group.
*/
constructor (parent, chartGroup) {
super();
this._select = undefined;
this._promptText = 'Select all';
this._multiple = false;
this._promptValue = null;
this._numberVisible = null;
this.data(group => group.all().filter(this._filterDisplayed));
this._filterDisplayed = d => this.valueAccessor()(d) > 0;
this._order = (a, b) => {
if (this.keyAccessor()(a) > this.keyAccessor()(b)) {
return 1;
}
if (this.keyAccessor()(a) < this.keyAccessor()(b)) {
return -1;
}
return 0;
};
this.anchor(parent, chartGroup);
}
_doRender () {
this.select('select').remove();
this._select = this.root().append('select')
.classed(SELECT_CSS_CLASS, true);
this._select.append('option').text(this._promptText).attr('value', '');
this._doRedraw();
return this;
}
_doRedraw () {
this._setAttributes();
this._renderOptions();
// select the option(s) corresponding to current filter(s)
if (this.hasFilter() && this._multiple) {
this._select.selectAll('option')
.property('selected', d => typeof d !== 'undefined' && this.filters().indexOf(String(this.keyAccessor()(d))) >= 0);
} else if (this.hasFilter()) {
this._select.property('value', this.filter());
} else {
this._select.property('value', '');
}
return this;
}
_renderOptions () {
const options = this._select.selectAll(`option.${OPTION_CSS_CLASS}`)
.data(this.data(), d => this.keyAccessor()(d));
options.exit().remove();
options.enter()
.append('option')
.classed(OPTION_CSS_CLASS, true)
.attr('value', d => this.keyAccessor()(d))
.merge(options)
.text(this.title());
this._select.selectAll(`option.${OPTION_CSS_CLASS}`).sort(this._order);
this._select.on('change', d3compat.eventHandler((d, evt) => this._onChange(d, evt)));
}
_onChange (_d, evt) {
let values;
const target = evt.target;
if (target.selectedOptions) {
const selectedOptions = Array.prototype.slice.call(target.selectedOptions);
values = selectedOptions.map(d => d.value);
} else { // IE and other browsers do not support selectedOptions
// adapted from this polyfill: https://gist.github.com/brettz9/4212217
const options = [].slice.call(evt.target.options);
values = options.filter(option => option.selected).map(option => option.value);
}
// console.log(values);
// check if only prompt option is selected
if (values.length === 1 && values[0] === '') {
values = this._promptValue || null;
} else if (!this._multiple && values.length === 1) {
values = values[0];
}
this.onChange(values);
}
onChange (val) {
if (val && this._multiple) {
this.replaceFilter([val]);
} else if (val) {
this.replaceFilter(val);
} else {
this.filterAll();
}
events.trigger(() => {
this.redrawGroup();
});
}
_setAttributes () {
if (this._multiple) {
this._select.attr('multiple', true);
} else {
this._select.attr('multiple', null);
}
if (this._numberVisible !== null) {
this._select.attr('size', this._numberVisible);
} else {
this._select.attr('size', null);
}
}
/**
* Get or set the function that controls the ordering of option tags in the
* select menu. By default options are ordered by the group key in ascending
* order.
* @param {Function} [order]
* @returns {Function|SelectMenu}
* @example
* // order by the group's value
* chart.order(function (a,b) {
* return a.value > b.value ? 1 : b.value > a.value ? -1 : 0;
* });
*/
order (order) {
if (!arguments.length) {
return this._order;
}
this._order = order;
return this;
}
/**
* Get or set the text displayed in the options used to prompt selection.
* @param {String} [promptText='Select all']
* @returns {String|SelectMenu}
* @example
* chart.promptText('All states');
*/
promptText (promptText) {
if (!arguments.length) {
return this._promptText;
}
this._promptText = promptText;
return this;
}
/**
* Get or set the function that filters option tags prior to display. By default options
* with a value of < 1 are not displayed.
* @param {function} [filterDisplayed]
* @returns {Function|SelectMenu}
* @example
* // display all options override the `filterDisplayed` function:
* chart.filterDisplayed(function () {
* return true;
* });
*/
filterDisplayed (filterDisplayed) {
if (!arguments.length) {
return this._filterDisplayed;
}
this._filterDisplayed = filterDisplayed;
return this;
}
/**
* Controls the type of select menu. Setting it to true converts the underlying
* HTML tag into a multiple select.
* @param {boolean} [multiple=false]
* @returns {boolean|SelectMenu}
* @example
* chart.multiple(true);
*/
multiple (multiple) {
if (!arguments.length) {
return this._multiple;
}
this._multiple = multiple;
return this;
}
/**
* Controls the default value to be used for
* [dimension.filter](https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension_filter)
* when only the prompt value is selected. If `null` (the default), no filtering will occur when
* just the prompt is selected.
* @param {?*} [promptValue=null]
* @returns {*|SelectMenu}
*/
promptValue (promptValue) {
if (!arguments.length) {
return this._promptValue;
}
this._promptValue = promptValue;
return this;
}
/**
* Controls the number of items to show in the select menu, when `.multiple()` is true. This
* controls the [`size` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#Attributes) of
* the `select` element. If `null` (the default), uses the browser's default height.
* @param {?number} [numberVisible=null]
* @returns {number|SelectMenu}
* @example
* chart.numberVisible(10);
*/
numberVisible (numberVisible) {
if (!arguments.length) {
return this._numberVisible;
}
this._numberVisible = numberVisible;
return this;
}
size (numberVisible) {
logger.warnOnce('selectMenu.size is ambiguous - use selectMenu.numberVisible instead');
if (!arguments.length) {
return this.numberVisible();
}
return this.numberVisible(numberVisible);
}
}
export const selectMenu = (parent, chartGroup) => new SelectMenu(parent, chartGroup);