Source: charts/html-legend.js

import {select} from 'd3-selection';

import {pluck, utils} from '../core/utils';
import {d3compat} from '../core/config';
import {constants} from '../core/constants';

/**
 * htmlLegend is a attachable widget that can be added to other dc charts to render horizontal/vertical legend
 * labels.
 * @example
 * chart.legend(HtmlLegend().container(legendContainerElement).horizontal(false))
 * @returns {HtmlLegend}
 */
export class HtmlLegend {
    constructor () {
        this._htmlLegendDivCssClass = 'dc-html-legend';
        this._legendItemCssClassHorizontal = 'dc-legend-item-horizontal';
        this._legendItemCssClassVertical = 'dc-legend-item-vertical';
        this._parent = undefined;
        this._container = undefined;
        this._legendText = pluck('name');
        this._maxItems = undefined;
        this._horizontal = false;
        this._legendItemClass = undefined;
        this._highlightSelected = false;
        this._keyboardAccessible = false;
    }

    parent (p) {
        if (!arguments.length) {
            return this._parent;
        }
        this._parent = p;
        return this;
    }

    render () {
        this._defaultLegendItemCssClass = this._horizontal ? this._legendItemCssClassHorizontal : this._legendItemCssClassVertical;
        this._container.select(`div.${this._htmlLegendDivCssClass}`).remove();

        const container = this._container.append('div').attr('class', this._htmlLegendDivCssClass);
        container.attr('style', `max-width:${this._container.nodes()[0].style.width}`);

        let legendables = this._parent.legendables();
        const filters = this._parent.filters();

        if (this._maxItems !== undefined) {
            legendables = legendables.slice(0, this._maxItems);
        }

        const legendItemClassName = this._legendItemClass ? this._legendItemClass : this._defaultLegendItemCssClass;

        const itemEnter = container.selectAll(`div.${legendItemClassName}`)
            .data(legendables).enter()
            .append('div')
            .classed(legendItemClassName, true)
            .on('mouseover', d3compat.eventHandler(d => this._parent.legendHighlight(d)))
            .on('mouseout', d3compat.eventHandler(d => this._parent.legendReset(d)))
            .on('click', d3compat.eventHandler(d => this._parent.legendToggle(d)));

        if (this._highlightSelected) {
            itemEnter.classed(constants.SELECTED_CLASS, d => filters.indexOf(d.name) !== -1);
        }

        itemEnter.append('span')
            .attr('class', 'dc-legend-item-color')
            .style('background-color', pluck('color'));

        itemEnter.append('span')
            .attr('class', 'dc-legend-item-label')
            .classed('dc-tabbable', this._keyboardAccessible)
            .attr('title', this._legendText)
            .text(this._legendText);

        if (this._keyboardAccessible) {
            this._makeLegendKeyboardAccessible();
        }
    }

    /**
     * Set the container selector for the legend widget. Required.
     * @param {String} [container]
     * @return {String|HtmlLegend}
     */
    container (container) {
        if (!arguments.length) {
            return this._container;
        }
        this._container = select(container);
        return this;
    }

    /**
     * This can be optionally used to override class for legenditem and just use this class style.
     * This is helpful for overriding the style of a particular chart rather than overriding
     * the style for all charts.
     *
     * Setting this will disable the highlighting of selected items also.
     * @param {String} [legendItemClass]
     * @return {String|HtmlLegend}
     */
    legendItemClass (legendItemClass) {
        if (!arguments.length) {
            return this._legendItemClass;
        }
        this._legendItemClass = legendItemClass;
        return this;
    }

    /**
     * This can be optionally used to enable highlighting legends for the selections/filters for the
     * chart.
     * @param {String} [highlightSelected]
     * @return {String|HtmlLegend}
     */
    highlightSelected (highlightSelected) {
        if (!arguments.length) {
            return this._highlightSelected;
        }
        this._highlightSelected = highlightSelected;
        return this;
    }

    /**
     * Display the legend horizontally instead of vertically
     * @param {String} [horizontal]
     * @return {String|HtmlLegend}
     */
    horizontal (horizontal) {
        if (!arguments.length) {
            return this._horizontal;
        }
        this._horizontal = horizontal;
        return this;
    }

    /**
     * Set or get the legend text function. The legend widget uses this function to render the legend
     * text for each item. If no function is specified the legend widget will display the names
     * associated with each group.
     * @param  {Function} [legendText]
     * @returns {Function|HtmlLegend}
     * @example
     * // default legendText
     * legend.legendText(pluck('name'))
     *
     * // create numbered legend items
     * chart.legend(new HtmlLegend().legendText(function(d, i) { return i + '. ' + d.name; }))
     *
     * // create legend displaying group counts
     * chart.legend(new HtmlLegend().legendText(function(d) { return d.name + ': ' d.data; }))
     */
    legendText (legendText) {
        if (!arguments.length) {
            return this._legendText;
        }
        this._legendText = legendText;
        return this;
    }

    /**
     * Maximum number of legend items to display
     * @param  {Number} [maxItems]
     * @return {HtmlLegend}
     */
    maxItems (maxItems) {
        if (!arguments.length) {
            return this._maxItems;
        }
        this._maxItems = utils.isNumber(maxItems) ? maxItems : undefined;
        return this;
    }

    /**
     * If set, individual legend items will be focusable from keyboard and on pressing Enter or Space
     * will behave as if clicked on.
     * 
     * If `svgDescription` on the parent chart has not been explicitly set, will also set the default 
     * SVG description text to the class constructor name, like BarChart or HeatMap, and make the entire
     * SVG focusable.
     * @param {Boolean} [keyboardAccessible=false]
     * @returns {Boolean|HtmlLegend}
     */
    keyboardAccessible (keyboardAccessible) {
        if (!arguments.length) {
            return this._keyboardAccessible;
        }
        this._keyboardAccessible = keyboardAccessible;
        return this;
    }

    _makeLegendKeyboardAccessible () {

        if (!this._parent._svgDescription) {

            this._parent.svg().append('desc')
                .attr('id', `desc-id-${this._parent.__dcFlag__}`)
                .html(`${this._parent.svgDescription()}`);

            this._parent.svg()
                .attr('tabindex', '0')
                .attr('role', 'img')
                .attr('aria-labelledby', `desc-id-${this._parent.__dcFlag__}`);
        }

        const tabElements = this.container()
            .selectAll('.dc-legend-item-label.dc-tabbable')
            .attr('tabindex', 0);

        tabElements
            .on('keydown', d3compat.eventHandler((d, event) => {
                // trigger only if d is an object
                if (event.keyCode === 13 && typeof d === 'object') {
                    d.chart.legendToggle(d)
                } 
                // special case for space key press - prevent scrolling
                if (event.keyCode === 32 && typeof d === 'object') {
                    d.chart.legendToggle(d)
                    event.preventDefault();            
                }
            }))
            .on('focus', d3compat.eventHandler(d => {
                this._parent.legendHighlight(d);
            }))
            .on('blur', d3compat.eventHandler(d => {
                this._parent.legendReset(d);
            }));
    }
}

export const htmlLegend = () => new HtmlLegend();