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);