import {select} from 'd3-selection'; import {events} from '../core/events'; import {BaseMixin} from '../base/base-mixin'; import {utils} from '../core/utils' import {d3compat} from '../core/config'; const GROUP_CSS_CLASS = 'dc-cbox-group'; const ITEM_CSS_CLASS = 'dc-cbox-item'; /** * The CboxMenu is a simple widget designed to filter a dimension by * selecting option(s) from a set of HTML `<input />` elements. The menu can be * made into a set of radio buttons (single select) or checkboxes (multiple). * @mixes BaseMixin */ export class CboxMenu extends BaseMixin { /** * Create a Cbox Menu. * * @example * // create a cboxMenu under #cbox-container using the default global chart group * var cbox = new CboxMenu('#cbox-container') * .dimension(states) * .group(stateGroup); * // the option text can be set via the title() function * // by default the option text is '`key`: `value`' * cbox.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._cbox = undefined; this._promptText = 'Select all'; this._multiple = false; this._inputType = 'radio'; this._promptValue = null; this._uniqueId = utils.uniqueId(); this.data(group => group.all().filter(this._filterDisplayed)); // There is an accessor for this attribute, initialized with default value 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 () { return this._doRedraw(); } _doRedraw () { this.select('ul').remove(); this._cbox = this.root() .append('ul') .classed(GROUP_CSS_CLASS, true); this._renderOptions(); if (this.hasFilter() && this._multiple) { this._cbox.selectAll('input') // adding `false` avoids failing test cases in phantomjs .property('checked', d => d && this.filters().indexOf(String(this.keyAccessor()(d))) >= 0 || false); } else if (this.hasFilter()) { this._cbox.selectAll('input') .property('checked', d => { if (!d) { return false; } return this.keyAccessor()(d) === this.filter(); }); } return this; } _renderOptions () { let options = this._cbox .selectAll(`li.${ITEM_CSS_CLASS}`) .data(this.data(), d => this.keyAccessor()(d)); options.exit().remove(); options = options.enter() .append('li') .classed(ITEM_CSS_CLASS, true) .merge(options); options .append('input') .attr('type', this._inputType) .attr('value', d => this.keyAccessor()(d)) .attr('name', `domain_${this._uniqueId}`) .attr('id', (d, i) => `input_${this._uniqueId}_${i}`); options .append('label') .attr('for', (d, i) => `input_${this._uniqueId}_${i}`) .text(this.title()); const chart = this; // 'all' option if (this._multiple) { this._cbox .append('li') .append('input') .attr('type', 'reset') .text(this._promptText) .on('click', d3compat.eventHandler(function (d, evt) { return chart._onChange(d, evt, this); })); } else { const li = this._cbox.append('li'); li.append('input') .attr('type', this._inputType) .attr('value', this._promptValue) .attr('name', `domain_${this._uniqueId}`) .attr('id', (d, i) => `input_${this._uniqueId}_all`) .property('checked', true); li.append('label') .attr('for', (d, i) => `input_${this._uniqueId}_all`) .text(this._promptText); } this._cbox .selectAll(`li.${ITEM_CSS_CLASS}`) .sort(this._order); this._cbox.on('change', d3compat.eventHandler(function (d, evt) { return chart._onChange(d, evt, this); })); return options; } _onChange (d, evt, element) { let values; const target = select(evt.target); let options; if (!target.datum()) { values = this._promptValue || null; } else { options = select(element).selectAll('input') .filter(function (o) { if (o) { return this.checked; } }); values = options.nodes().map(option => option.value); // check if only prompt option is selected 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(); }); } /** * Get or set the function that controls the ordering of option tags in the * cbox menu. By default options are ordered by the group key in ascending * order. * @param {Function} [order] * @returns {Function|CboxMenu} * @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|CboxMenu} * @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 options prior to display. By default options * with a value of < 1 are not displayed. * @param {function} [filterDisplayed] * @returns {Function|CboxMenu} * @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 input element. Setting it to true converts * the HTML `input` tags from radio buttons to checkboxes. * @param {boolean} [multiple=false] * @returns {Boolean|CboxMenu} * @example * chart.multiple(true); */ multiple (multiple) { if (!arguments.length) { return this._multiple; } this._multiple = multiple; if (this._multiple) { this._inputType = 'checkbox'; } else { this._inputType = 'radio'; } 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 {*|CboxMenu} */ promptValue (promptValue) { if (!arguments.length) { return this._promptValue; } this._promptValue = promptValue; return this; } } export const cboxMenu = (parent, chartGroup) => new CboxMenu(parent, chartGroup);