Source: charts/cbox-menu.js

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