Source: muze-utils/src/renderer-utils.js

import { select, event as d3event, selection, clientPoint } from 'd3-selection';
import { drag as d3drag } from 'd3-drag';
import 'd3-transition';
import { mergeRecursive } from './common-utils';
/**
 * This function takes a raw DOM element or
 * a string and returns a d3 selection of that element.
 *
 * @param {HTMLElement | string} element The element to wrap in d3 selection.
 * @return {Selection} Instance of d3 selection.
 */
const selectElement = element => select(element);

/**
 * It creates a new d3 element on the parent element
 * @param  {Selection} parent Element to which the element is to be appended
 * @param  {Object} elemType Type of the new element
 * @param  {Array} data Data for the new element
 * @param  {Object} selector classname/id/tagname for the element
 * @return {HTMLElement} Newly Created Element
 */
const makeElement = (parent, elemType, data, selector, callbacks = {}, keyFn) => {
    if ((parent instanceof HTMLElement || parent instanceof SVGElement)) {
        parent = selectElement(parent);
    }

    const selectorVal = selector ? selector[0] : null;
    let selectorType = null;
    let actualSelector = null;
    let element = null;
    let enterSel = null;
    let mergeSel = null;
    let filter;
    if (selectorVal) {
        if (selectorVal === '#') {
            selectorType = 'id';
            actualSelector = selector;
        } else {
            selectorType = 'class';
            actualSelector = selector[0] === '.' ? selector : `.${selector}`;
        }
    } else {
        actualSelector = elemType;
        filter = true;
    }
    element = parent.selectAll(actualSelector);

    filter && (element = element.filter(function () {
        return this.parentNode === parent.node();
    }));
    element = element.data(data, keyFn);

    enterSel = element.enter()
                            .append(elemType || 'div');
    callbacks.enter && enterSel.each(function (...params) {
        callbacks.enter(selectElement(this), ...params);
    });

    mergeSel = enterSel.merge(element);
    callbacks.update && mergeSel.each(function (...params) {
        callbacks.update(selectElement(this), ...params);
    });
    if (selectorType === 'class') {
        mergeSel.classed(selectorVal === '.' ? selector.substring(1, selector.length) : selector, true);
    } else if (selectorType === 'id') {
        mergeSel.attr('id', selector.substring(1, selector.length));
    }
    const exitSel = element.exit();

    if (callbacks.exit) {
        exitSel.each(function (...params) {
            callbacks.exit(selectElement(this), ...params);
        });
    } else {
        exitSel.remove();
    }
    return mergeSel;
};

/**
 * This function applies styles to an element
 *  @param  {Object} elem Element Selection
 * @param  {Object} styleObj Style Object to be applied
 * @return {Element} Newly Created Element
*/
const applyStyle = (elem, styleObj) => {
    Object.entries(styleObj).forEach((d) => {
        elem.style(d[0], d[1]);
    }, this);
    return elem;
};

/**
 * This function appends a dom element in another element.
 * @param {HTMLElement | SVGElement} element1 The element in which to append the second element
 * @param {HTMLElement | SVGElement} element2 The element which will be appended.
 */
const appendElement = (element1, element2) => {
    element1.appendChild(element2);
};

/**
 * Sets the attributes to the element.
 * @param {HTMLElement | SVGElement} element Element on which attributes will be applied
 * @param {Object} attrs Attributes which need to be applied
 */
const setElementAttrs = (element, attrs) => {
    for (const key in attrs) {
        if ({}.hasOwnProperty.call(attrs, key)) {
            element.attr(key, attrs[key]);
        }
    }
};

/**
 * Sets attributes in the svg or html element
 * @param {SVGElement | HTMLElement} element The element on which attrs will be applied
 * @param {Array.<Object>} attrs Array of attributes
 * @param {string} className className of elements to select.
 * @return {SVGElement | HTMLElement} SVGElement or html element.
 */
const setAttrs = (element, attrs, className) => {
    if (!(element instanceof selection)) {
        element = selectElement(element);
    }
    className !== undefined ? element.selectAll(`.${className}`).each(function () {
        setElementAttrs(select(this), attrs);
    }) : setElementAttrs(element, attrs);
    return element;
};

/**
 * Adds a css class to elements which passes the filter function.If filter function is not given,
 * then all elements will be applied the class.
 * @param {HTMLElement | SVGElement} element element
 * @param {string} className css class
 * @param {string} selector css selector
 * @param {Function} filterFn filter method.
 */
const addClass = (element, className, selector, filterFn) => {
    select(element).selectAll(selector).each(function (data) {
        const elem = select(this);
        filterFn ? filterFn(data) && elem.classed(className, true) :
                elem.classed(className, true);
    });
};

/**
 * Removes the css class from elements which passes the filter function.If filter function is not given,
 * then all elements will be applied the class.
 * @param {HTMLElement | SVGElement} element element
 * @param {string} className css class
 * @param {string} selector css selector
 * @param {Function} filterFn filter method.
 */
const removeClass = (element, className, selector, filterFn) => {
    select(element).selectAll(selector).each(function (data) {
        const elem = select(this);
        filterFn ? filterFn(data) && elem.classed(className, false) :
                elem.classed(className, false);
    });
};

/**
 * Sets styles in the svg or html element
 * @param {SVGElement | HTMLElement} element The element on which styles will be applied
 * @param {Array.<Object>} styles Array of style attributes
 * @return {SVGElement | HTMLElement} SVGElement or html element.
 */
const setStyles = (element, styles) => {
    if (!(element instanceof selection)) {
        element = selectElement(element);
    }
    for (const key in styles) {
        if ({}.hasOwnProperty.call(styles, key)) {
            element.style(key, styles[key]);
        }
    }
    return element;
};

/**
 * Creates svg or html elements by binding data to the selection.Removes extra elements
 * on exit.
 * @param {Object} params Parameter object
 * @param {SVGElement | HTMLElement} params.container Container element where all elements will be appended
 * @param {string} params.selector Selector by which elements will be selected from dom
 * @param {string} params.append Append tag
 * @param {Function} params.each Function which will be executed for each data point and element.
 */
const createElements = (params) => {
    const container = select(params.container);
    let data;
    let sel;

    data = params.data;
    if (typeof data === 'number') {
        data = Array(data).fill().map((d, i) => i);
    }
    sel = container.selectAll(params.selector).data(data);

    sel = container.selectAll(params.selector).filter(function () {
        return this.parentNode === container.node();
    }).data(params.data);

    const selectionMerge = sel.enter().append(params.append).merge(sel).each(function (d, i) {
        params.each(d, select(this), i);
    });

    selectionMerge.attr('class', params.className || '');
    sel.exit().remove();
};

/**
 * Clips an element with given measurement. Basically it is used to hide overflowing portions
 * of any element.
 * @param {SVGElement} container svg element which needs to be clipped.
 * @param {Object} measurement Dimensions of the clipped rectangle.
 * @param {string} id Unique id of the clip element
 */
const clipElement = (container, measurement, id) => {
    const clipPathElement = makeElement(select(container), 'clipPath', [1], `#${id}`);
    const clipPathRect = makeElement(clipPathElement, 'rect', [1]);
    clipPathRect.attr('x', measurement.x)
                    .attr('y', measurement.x)
                    .attr('width', measurement.width)
                    .attr('height', measurement.height);
    clipPathElement.attr('clip-path', `url(#${id})`);
};

/**
 * Returns the element of the corresponding classname.
 * @param {HTMLElement | SVGElement} node html or svg node element.
 * @param {string} className css class to be applied.
 * @return {HTMLElement | SVGElement} html or svg element.
 */
const getElementsByClassName = (node, className) => select(node).selectAll(className).nodes();

/**
 * Gets the mouse position relative to an svg type of element
 * @param {SVGElement} element Any svg element like rect, circle, etc.
 * @param {Event} event Event object.
 * @return {Object} x and y position relative to the container element passed.
 */
const getMousePos = (element, event) => {
    const boundingClientRect = element.getBoundingClientRect();
    return {
        x: event.x - boundingClientRect.x,
        y: event.y - boundingClientRect.y
    };
};

const getClientPoint = (...params) => {
    const pos = clientPoint(...params);
    return {
        x: pos[0],
        y: pos[1]
    };
};

/** This function appends a dom element in another element.
 * @param {Object} tag The tag name of the element to append
 * @param {Object} mount Mount point fo the element
 * @return {Object} selection of the appended element
 */
const createElement = (tag, mount) => select(mount).append(tag).node();

/**
 * Gets the d3 event function
 * @return {Object} d3 event
 */
const getEvent = () => d3event;

/**
 * Gets the d3 drag function
 * @return {Object} d3 drag
 */
const getD3Drag = () => d3drag;

const getSmartComputedStyle = (group, css) => {
    let textEl;
    const testText = 'W';
    const mandatoryStyle = {
        'fill-opacity': 0
    };
    const className = typeof css === 'string' ? css : (css instanceof Array ? css.join(' ') : undefined);

    if (group.node() instanceof HTMLElement) {
        textEl = group.append('div').html(testText);
    } else {
        textEl = group.append('text').text(testText);
    }

    if (className) {
        textEl.attr('class', className);
    } else if (typeof css === 'object') {
        delete css['fill-opacity'];
        mergeRecursive(mandatoryStyle, css);
    }

    textEl.style(mandatoryStyle);

    const computedStyle = window.getComputedStyle(textEl.node());
    const styleForSmartLabel = {
        fontSize: computedStyle.fontSize,
        fontFamily: computedStyle.fontFamily,
        fontWeight: computedStyle.fontWeight,
        fontStyle: computedStyle.fontStyle
    };

    textEl.remove();

    return styleForSmartLabel;
};

const hasTouch = () => 'ontouchstart' in document.documentElement;

export {
    hasTouch,
    selectElement,
    makeElement,
    applyStyle,
    addClass,
    removeClass,
    appendElement,
    setAttrs,
    setStyles,
    createElement,
    createElements,
    clipElement,
    getElementsByClassName,
    getMousePos,
    getEvent,
    getD3Drag,
    getSmartComputedStyle,
    getClientPoint
};