<commit_before>/**
 * @fileoverview Plot component.
 * @author NHN Ent.
 *         FE Development Lab <dl_javascript@nhnent.com>
 */

'use strict';

var dom = require('../helpers/domHandler');
var predicate = require('../helpers/predicate');
var calculator = require('../helpers/calculator');
var renderUtil = require('../helpers/renderUtil');
var plotTemplate = require('./plotTemplate');

var Plot = tui.util.defineClass(/** @lends Plot.prototype */ {
    /**
     * Plot component.
     * @constructs Plot
     * @param {object} params parameters
     *      @param {number} params.vTickCount vertical tick count
     *      @param {number} params.hTickCount horizontal tick count
     *      @param {object} params.theme axis theme
     */
    init: function(params) {
        /**
         * Plot view className
         * @type {string}
         */
        this.className = 'tui-chart-plot-area';

        /**
         * Bounds maker
         * @type {BoundsMaker}
         */
        this.boundsMaker = params.boundsMaker;

        /**
         * Data processor
         * @type {DataProcessor}
         */
        this.dataProcessor = params.dataProcessor;

        /**
         * Options
         * @type {object}
         */
        this.options = params.options || {};
        this.options.showLine = tui.util.isUndefined(this.options.showLine) ? true : this.options.showLine;
        this.options.lines = this.options.lines || [];
        this.options.bands = this.options.bands || [];

        /**
         * x axis type
         * @type {?string}
         */
        this.xAxisType = params.xAxisType;

        /**
         * Theme
         * @type {object}
         */
        this.theme = params.theme || {};

        /**
         * chart type
         * @type {string}
         */
        this.chartType = params.chartType;

        /**
         * sub charts type
         * @type {Array.<string>}
         */
        this.chartTypes = params.chartTypes;

        /**
         * whether vertical or not
         */
        this.isVertical = params.isVertical;
    },

    /**
     * Render plot area.
     * @param {HTMLElement} plotContainer plot area element
     * @param {object} data rendering data
     * @private
     */
    _renderPlotArea: function(plotContainer) {
        var dimension = this.boundsMaker.getDimension('plot');

        renderUtil.renderDimension(plotContainer, dimension);
        renderUtil.renderPosition(plotContainer, this.boundsMaker.getPosition('plot'));

        if (predicate.isLineTypeChart(this.chartType, this.chartTypes)) {
            this._renderOptionalLines(plotContainer, dimension);
        }

        if (this.options.showLine) {
            this._renderPlotLines(plotContainer, dimension);
        }
    },

    /**
     * Render plot component.
     * @param {object} data rendering data
     * @returns {HTMLElement} plot element
     */
    render: function() {
        var container = dom.create('DIV', this.className);

        this._renderPlotArea(container);
        this.plotContainer = container;

        return container;
    },

    /**
     * Rerender.
     * @param {object} data rendering
     */
    rerender: function() {
        this.plotContainer.innerHTML = '';
        this._renderPlotArea(this.plotContainer);
    },

    /**
     * Resize plot component.
     * @param {object} data rendering data
     */
    resize: function(data) {
        this.rerender(data);
    },

    /**
     * Make template params for vertical line.
     * @param {object} additionalParams - additional params
     * @returns {object}
     * @private
     */
    _makeVerticalLineTemplateParams: function(additionalParams) {
        return tui.util.extend({
            className: 'vertical',
            positionType: 'left',
            width: '1px'
        }, additionalParams);
    },

    /**
     * Make template params for horizontal line.
     * @param {object} additionalParams - additional params
     * @returns {object}
     * @private
     */
    _makeHorizontalLineTemplateParams: function(additionalParams) {
        return tui.util.extend({
            className: 'horizontal',
            positionType: 'bottom',
            height: '1px'
        }, additionalParams);
    },

    /**
     * Make line html.
     * @param {number} position - position value
     * @param {number} standardWidth - standard width
     * @param {object} templateParams - template parameters
     * @returns {string}
     * @private
     */
    _makeLineHtml: function(position, standardWidth, templateParams) {
        var percentagePosition = calculator.makePercentageValue(position, standardWidth);

        templateParams.positionValue = percentagePosition + '%';

        return plotTemplate.tplPlotLine(templateParams);
    },

    /**
     * Create value range for optional line.
     * @param {{range: ?Array.<number>, value: ?number}} optionalLineData - optional line data
     * @returns {Array.<number>}
     * @private
     */
    _createOptionalLineValueRange: function(optionalLineData) {
        var range = optionalLineData.range || [optionalLineData.value];

        if (predicate.isDatetimeType(this.xAxisType)) {
            range = tui.util.map(range, function(value) {
                var date = new Date(value);

                return date.getTime() || value;
            });
        }

        return range;
    },

    /**
     * Create position for optional line, when value axis.
     * @param {{dataMin: number, distance: number}} xAxisData - x axis data
     * @param {number} width - width
     * @param {number} value - value
     * @returns {number}
     * @private
     */
    _createOptionalLinePosition: function(xAxisData, width, value) {
        var ratio = (value - xAxisData.dataMin) / xAxisData.distance;
        var position = ratio * width;

        if (ratio === 1) {
            position -= 1;
        }

        if (position < 0) {
            position = null;
        }

        return position;
    },

    /**
     * Create position for optional line, when label axis.
     * @param {number} width - width
     * @param {number} value - value
     * @returns {number}
     * @private
     */
    _createOptionalLinePositionWhenLabelAxis: function(width, value) {
        var dataProcessor = this.dataProcessor;
        var index = dataProcessor.findCategoryIndex(value);
        var position = null;
        var ratio;

        if (!tui.util.isNull(index)) {
            ratio = (index === 0) ? 0 : (index / (dataProcessor.getCategoryCount() - 1));
            position = ratio * width;
        }

        if (ratio === 1) {
            position -= 1;
        }

        return position;
    },

    /**
     * Create position map for optional line.
     * @param {{range: ?Array.<number>, value: ?number}} optionalLineData - optional line data
     * @param {{isLabelAxis: boolean, dataMin: number, distance: number}} xAxisData - x axis data
     * @param {number} width - width
     * @returns {{start: number, end: number}}
     * @private
     */
    _createOptionalLinePositionMap: function(optionalLineData, xAxisData, width) {
        var range = this._createOptionalLineValueRange(optionalLineData);
        var startPosition, endPosition;

        if (xAxisData.isLabelAxis) {
            startPosition = this._createOptionalLinePositionWhenLabelAxis(width, range[0]);
            endPosition = this._createOptionalLinePositionWhenLabelAxis(width, range[1]);
        } else {
            startPosition = this._createOptionalLinePosition(xAxisData, width, range[0]);
            endPosition = range[1] && this._createOptionalLinePosition(xAxisData, width, range[1]);
        }

        if (tui.util.isExisty(endPosition) && tui.util.isNull(startPosition)) {
            startPosition = 0;
        }

        return {
            start: startPosition,
            end: endPosition
        };
    },

    /**
     * Make optional line html.
     * @param {Array.<number>} xAxisData - positions
     * @param {number} width - standard width
     * @param {object} templateParams - template parameters
     * @param {object} optionalLineData - optional line information
     * @returns {string}
     * @private
     */
    _makeOptionalLineHtml: function(xAxisData, width, templateParams, optionalLineData) {
        var positionMap = this._createOptionalLinePositionMap(optionalLineData, xAxisData, width);
        var html = '';
        var percentageWidth;

        if (tui.util.isExisty(positionMap.start) && (positionMap.start >= 0) && (positionMap.start <= width)) {
            if (tui.util.isExisty(positionMap.end)) {
                percentageWidth = calculator.makePercentageValue(positionMap.end - positionMap.start, width);
                templateParams.width = percentageWidth + '%';
            } else {
                templateParams.width = '1px';
            }

            templateParams.color = optionalLineData.color || 'transparent';
            html = this._makeLineHtml(positionMap.start, width, templateParams);
        }

        return html;
    },

    /**
     * Make optional lines html.
     * @param {Array.<object>} lines - optional lines
     * @param {{width: number, height: number}} dimension - dimension
     * @returns {string}
     * @private
     */
    _makeOptionalLinesHtml: function(lines, dimension) {
        var self = this;
        var width = dimension.width;
        var xAxisData = this.boundsMaker.getAxesData().xAxis;
        var templateParams = self._makeVerticalLineTemplateParams({
            height: dimension.height + 'px'
        });
        var makeOptionalLineHtml = tui.util.bind(this._makeOptionalLineHtml, this, xAxisData, width, templateParams);

        return tui.util.map(lines, makeOptionalLineHtml).join('');
    },

    /**
     * Render optional lines and bands.
     * @param {HTMLElement} container - container
     * @param {{width: number, height: number}} dimension - dimension
     * @private
     */
    _renderOptionalLines: function(container, dimension) {
        var optionalContainer = dom.create('DIV', 'tui-chart-plot-optional-lines-area');
        var bandsHtml = this._makeOptionalLinesHtml(this.options.bands, dimension);
        var linesHtml = this._makeOptionalLinesHtml(this.options.lines, dimension);

        this.optionalContainer = optionalContainer;

        dom.append(container, optionalContainer);

        optionalContainer.innerHTML = bandsHtml + linesHtml;
    },

    /**
     * Make html of plot lines.
     * @param {Array.<number>} positions - position values
     * @param {number} standardWidth - standard width
     * @param {object} templateParams parameters
     * @returns {string} html
     * @private
     */
    _makeLinesHtml: function(positions, standardWidth, templateParams) {
        var self = this;
        var lineHtml = tui.util.map(positions, function(position) {
            return self._makeLineHtml(position, standardWidth, templateParams);
        }).join('');

        return lineHtml;
    },

    /**
     * Maker html for vertical lines
     * @param {{width: number, height: number}} dimension - dimension
     * @param {string} lineColor - line color
     * @returns {string}
     * @private
     */
    _makeVerticalLinesHtml: function(dimension, lineColor) {
        var positions = this._makeHorizontalPositions(dimension.width);
        var templateParams = this._makeVerticalLineTemplateParams({
            height: dimension.height + 'px',
            color: lineColor
        });

        return this._makeLinesHtml(positions, dimension.width, templateParams);
    },
    /**
     * Maker html for horizontal lines
     * @param {{width: number, height: number}} dimension - dimension
     * @param {string} lineColor - line color
     * @returns {string}
     * @private
     */
    _makeHorizontalLinesHtml: function(dimension, lineColor) {
        var positions = this._makeVerticalPositions(dimension.height);
        var templateParams = this._makeHorizontalLineTemplateParams({
            width: dimension.width + 'px',
            color: lineColor
        });

        return this._makeLinesHtml(positions, dimension.height, templateParams);
    },

    /**
     * Render plot lines.
     * @param {HTMLElement} container - container element
     * @param {{width: number, height: number}} dimension plot area dimension
     * @private
     */
    _renderPlotLines: function(container, dimension) {
        var lineContainer = dom.create('DIV', 'tui-chart-plot-lines-area');
        var theme = this.theme;
        var lineHtml = '';

        if (!predicate.isLineTypeChart(this.chartType)) {
            lineHtml += this._makeVerticalLinesHtml(dimension, theme.lineColor);
        }

        lineHtml += this._makeHorizontalLinesHtml(dimension, theme.lineColor);

        dom.append(container, lineContainer);
        lineContainer.innerHTML += lineHtml;
        renderUtil.renderBackground(container, theme.background);
    },

    /**
     * Make positions for vertical line.
     * @param {number} height plot height
     * @returns {Array.<number>} positions
     * @private
     */
    _makeVerticalPositions: function(height) {
        var axesData = this.boundsMaker.getAxesData();
        var yAxis = axesData.yAxis || axesData.rightYAxis;
        var positions = calculator.makeTickPixelPositions(height, yAxis.validTickCount);

        positions.shift();

        return positions;
    },

    /**
     * Make divided positions of plot.
     * @param {number} width - plot width
     * @param {number} tickCount - tick count
     * @returns {Array.<number>}
     * @private
     */
    _makeDividedPlotPositions: function(width, tickCount) {
        var yAxisWidth = this.boundsMaker.getDimension('yAxis').width;
        var leftWidth, rightWidth, leftPositions, rightPositions;

        tickCount = parseInt(tickCount / 2, 10) + 1;
        width -= yAxisWidth;
        leftWidth = Math.round((width) / 2);
        rightWidth = width - leftWidth;

        leftPositions = calculator.makeTickPixelPositions(leftWidth, tickCount);
        rightPositions = calculator.makeTickPixelPositions(rightWidth, tickCount, leftWidth + yAxisWidth);

        leftPositions.pop();
        rightPositions.shift();

        return leftPositions.concat(rightPositions);
    },

    /**
     * Make positions for horizontal line.
     * @param {number} width plot width
     * @returns {Array.<number>} positions
     * @private
     */
    _makeHorizontalPositions: function(width) {
        var tickCount = this.boundsMaker.getAxesData().xAxis.validTickCount;
        var positions;

        if (this.options.divided) {
            positions = this._makeDividedPlotPositions(width, tickCount);
        } else {
            positions = calculator.makeTickPixelPositions(width, tickCount);
            positions.shift();
        }

        return positions;
    },

    /**
     * Add plot line.
     * @param {{index: number, color: string, id: string}} data - data
     */
    addPlotLine: function(data) {
        this.options.lines.push(data);
        this.rerender();
    },

    /**
     * Add plot band.
     * @param {{range: [number, number], color: string, id: string}} data - data
     */
    addPlotBand: function(data) {
        this.options.bands.push(data);
        this.rerender();
    },

    /**
     * Remove plot line.
     * @param {string} id - line id
     */
    removePlotLine: function(id) {
        this.options.lines = tui.util.filter(this.options.lines, function(line) {
            return line.id !== id;
        });
        this.rerender();
    },

    /**
     * Remove plot band.
     * @param {string} id - band id
     */
    removePlotBand: function(id) {
        this.options.bands = tui.util.filter(this.options.bands, function(line) {
            return line.id !== id;
        });
        this.rerender();
    },

    animateForAddingData: function(data) {
        var self = this;
        var beforeLeft = 0;
        var interval = data.tickSize;
        var areaWidth;

        if (this.dataProcessor.isCoordinateType()) {
            this.optionalContainer.innerHTML = '';
        } else if (data.shifting) {
            renderUtil.startAnimation(300, function(ratio) {
                var left = interval * ratio;
                self.optionalContainer.style.left = (beforeLeft - left) + 'px';
            });
        } else {
            areaWidth = this.boundsMaker.getDimension('plot').width;
            renderUtil.startAnimation(300, function(ratio) {
                var left = interval * ratio;
                self.optionalContainer.style.width = (areaWidth - left) + 'px';
            }, function() {
            });
        }
    }
});

module.exports = Plot;
<commit_msg>refactor: Apply code review result https://github.nhnent.com/fe/tui.chart/pull/364<commit_after>/**
 * @fileoverview Plot component.
 * @author NHN Ent.
 *         FE Development Lab <dl_javascript@nhnent.com>
 */

'use strict';

var dom = require('../helpers/domHandler');
var predicate = require('../helpers/predicate');
var calculator = require('../helpers/calculator');
var renderUtil = require('../helpers/renderUtil');
var plotTemplate = require('./plotTemplate');

var Plot = tui.util.defineClass(/** @lends Plot.prototype */ {
    /**
     * Plot component.
     * @constructs Plot
     * @param {object} params parameters
     *      @param {number} params.vTickCount vertical tick count
     *      @param {number} params.hTickCount horizontal tick count
     *      @param {object} params.theme axis theme
     */
    init: function(params) {
        /**
         * Plot view className
         * @type {string}
         */
        this.className = 'tui-chart-plot-area';

        /**
         * Bounds maker
         * @type {BoundsMaker}
         */
        this.boundsMaker = params.boundsMaker;

        /**
         * Data processor
         * @type {DataProcessor}
         */
        this.dataProcessor = params.dataProcessor;

        /**
         * Options
         * @type {object}
         */
        this.options = params.options || {};
        this.options.showLine = tui.util.isUndefined(this.options.showLine) ? true : this.options.showLine;
        this.options.lines = this.options.lines || [];
        this.options.bands = this.options.bands || [];

        /**
         * x axis type
         * @type {?string}
         */
        this.xAxisType = params.xAxisType;

        /**
         * Theme
         * @type {object}
         */
        this.theme = params.theme || {};

        /**
         * chart type
         * @type {string}
         */
        this.chartType = params.chartType;

        /**
         * sub charts type
         * @type {Array.<string>}
         */
        this.chartTypes = params.chartTypes;

        /**
         * whether vertical or not
         */
        this.isVertical = params.isVertical;
    },

    /**
     * Render plot area.
     * @param {HTMLElement} plotContainer plot area element
     * @private
     */
    _renderPlotArea: function(plotContainer) {
        var dimension = this.boundsMaker.getDimension('plot');

        renderUtil.renderDimension(plotContainer, dimension);
        renderUtil.renderPosition(plotContainer, this.boundsMaker.getPosition('plot'));

        if (predicate.isLineTypeChart(this.chartType, this.chartTypes)) {
            this._renderOptionalLines(plotContainer, dimension);
        }

        if (this.options.showLine) {
            this._renderPlotLines(plotContainer, dimension);
        }
    },

    /**
     * Render plot component.
     * @returns {HTMLElement} plot element
     */
    render: function() {
        var container = dom.create('DIV', this.className);

        this._renderPlotArea(container);
        this.plotContainer = container;

        return container;
    },

    /**
     * Rerender.
     * @param {object} data rendering
     */
    rerender: function() {
        this.plotContainer.innerHTML = '';
        this._renderPlotArea(this.plotContainer);
    },

    /**
     * Resize plot component.
     * @param {object} data rendering data
     */
    resize: function(data) {
        this.rerender(data);
    },

    /**
     * Make template params for vertical line.
     * @param {object} additionalParams - additional params
     * @returns {object}
     * @private
     */
    _makeVerticalLineTemplateParams: function(additionalParams) {
        return tui.util.extend({
            className: 'vertical',
            positionType: 'left',
            width: '1px'
        }, additionalParams);
    },

    /**
     * Make template params for horizontal line.
     * @param {object} additionalParams - additional params
     * @returns {object}
     * @private
     */
    _makeHorizontalLineTemplateParams: function(additionalParams) {
        return tui.util.extend({
            className: 'horizontal',
            positionType: 'bottom',
            height: '1px'
        }, additionalParams);
    },

    /**
     * Make line html.
     * @param {number} position - position value
     * @param {number} standardWidth - standard width
     * @param {object} templateParams - template parameters
     * @returns {string}
     * @private
     */
    _makeLineHtml: function(position, standardWidth, templateParams) {
        var percentagePosition = calculator.makePercentageValue(position, standardWidth);

        templateParams.positionValue = percentagePosition + '%';

        return plotTemplate.tplPlotLine(templateParams);
    },

    /**
     * Create value range for optional line.
     * @param {{range: ?Array.<number>, value: ?number}} optionalLineData - optional line data
     * @returns {Array.<number>}
     * @private
     */
    _createOptionalLineValueRange: function(optionalLineData) {
        var range = optionalLineData.range || [optionalLineData.value];

        if (predicate.isDatetimeType(this.xAxisType)) {
            range = tui.util.map(range, function(value) {
                var date = new Date(value);

                return date.getTime() || value;
            });
        }

        return range;
    },

    /**
     * Create position for optional line, when value axis.
     * @param {{dataMin: number, distance: number}} xAxisData - x axis data
     * @param {number} width - width
     * @param {number} value - value
     * @returns {number|null}
     * @private
     */
    _createOptionalLinePosition: function(xAxisData, width, value) {
        var ratio = (value - xAxisData.dataMin) / xAxisData.distance;
        var position = ratio * width;

        if (ratio === 1) {
            position -= 1;
        }

        if (position < 0) {
            position = null;
        }

        return position;
    },

    /**
     * Create position for optional line, when label axis.
     * @param {number} width - width
     * @param {number} value - value
     * @returns {number|null}
     * @private
     */
    _createOptionalLinePositionWhenLabelAxis: function(width, value) {
        var dataProcessor = this.dataProcessor;
        var index = dataProcessor.findCategoryIndex(value);
        var position = null;
        var ratio;

        if (!tui.util.isNull(index)) {
            ratio = (index === 0) ? 0 : (index / (dataProcessor.getCategoryCount() - 1));
            position = ratio * width;
        }

        if (ratio === 1) {
            position -= 1;
        }

        return position;
    },

    /**
     * Create position map for optional line.
     * @param {{range: ?Array.<number>, value: ?number}} optionalLineData - optional line data
     * @param {{isLabelAxis: boolean, dataMin: number, distance: number}} xAxisData - x axis data
     * @param {number} width - width
     * @returns {{start: number, end: number}}
     * @private
     */
    _createOptionalLinePositionMap: function(optionalLineData, xAxisData, width) {
        var range = this._createOptionalLineValueRange(optionalLineData);
        var startPosition, endPosition;

        if (xAxisData.isLabelAxis) {
            startPosition = this._createOptionalLinePositionWhenLabelAxis(width, range[0]);
            endPosition = this._createOptionalLinePositionWhenLabelAxis(width, range[1]);
        } else {
            startPosition = this._createOptionalLinePosition(xAxisData, width, range[0]);
            endPosition = range[1] && this._createOptionalLinePosition(xAxisData, width, range[1]);
        }

        if (tui.util.isExisty(endPosition) && tui.util.isNull(startPosition)) {
            startPosition = 0;
        }

        return {
            start: startPosition,
            end: endPosition
        };
    },

    /**
     * Make optional line html.
     * @param {Array.<number>} xAxisData - positions
     * @param {number} width - standard width
     * @param {object} templateParams - template parameters
     * @param {object} optionalLineData - optional line information
     * @returns {string}
     * @private
     */
    _makeOptionalLineHtml: function(xAxisData, width, templateParams, optionalLineData) {
        var positionMap = this._createOptionalLinePositionMap(optionalLineData, xAxisData, width);
        var plotLineWidth = '1px';
        var html = '';
        var percentageWidth;

        if (tui.util.isExisty(positionMap.start) && (positionMap.start >= 0) && (positionMap.start <= width)) {
            if (tui.util.isExisty(positionMap.end)) {
                percentageWidth = calculator.makePercentageValue(positionMap.end - positionMap.start, width);
                templateParams.width = percentageWidth + '%';
            } else {
                templateParams.width = plotLineWidth;
            }

            templateParams.color = optionalLineData.color || 'transparent';
            html = this._makeLineHtml(positionMap.start, width, templateParams);
        }

        return html;
    },

    /**
     * Make optional lines html.
     * @param {Array.<object>} lines - optional lines
     * @param {{width: number, height: number}} dimension - dimension
     * @returns {string}
     * @private
     */
    _makeOptionalLinesHtml: function(lines, dimension) {
        var self = this;
        var width = dimension.width;
        var xAxisData = this.boundsMaker.getAxesData().xAxis;
        var templateParams = self._makeVerticalLineTemplateParams({
            height: dimension.height + 'px'
        });
        var makeOptionalLineHtml = tui.util.bind(this._makeOptionalLineHtml, this, xAxisData, width, templateParams);

        return tui.util.map(lines, makeOptionalLineHtml).join('');
    },

    /**
     * Render optional lines and bands.
     * @param {HTMLElement} container - container
     * @param {{width: number, height: number}} dimension - dimension
     * @private
     */
    _renderOptionalLines: function(container, dimension) {
        var optionalContainer = dom.create('DIV', 'tui-chart-plot-optional-lines-area');
        var bandsHtml = this._makeOptionalLinesHtml(this.options.bands, dimension);
        var linesHtml = this._makeOptionalLinesHtml(this.options.lines, dimension);

        this.optionalContainer = optionalContainer;

        dom.append(container, optionalContainer);

        optionalContainer.innerHTML = bandsHtml + linesHtml;
    },

    /**
     * Make html of plot lines.
     * @param {Array.<number>} positions - position values
     * @param {number} standardWidth - standard width
     * @param {object} templateParams parameters
     * @returns {string} html
     * @private
     */
    _makeLinesHtml: function(positions, standardWidth, templateParams) {
        var self = this;
        var lineHtml = tui.util.map(positions, function(position) {
            return self._makeLineHtml(position, standardWidth, templateParams);
        }).join('');

        return lineHtml;
    },

    /**
     * Maker html for vertical lines
     * @param {{width: number, height: number}} dimension - dimension
     * @param {string} lineColor - line color
     * @returns {string}
     * @private
     */
    _makeVerticalLinesHtml: function(dimension, lineColor) {
        var positions = this._makeHorizontalPositions(dimension.width);
        var templateParams = this._makeVerticalLineTemplateParams({
            height: dimension.height + 'px',
            color: lineColor
        });

        return this._makeLinesHtml(positions, dimension.width, templateParams);
    },
    /**
     * Maker html for horizontal lines
     * @param {{width: number, height: number}} dimension - dimension
     * @param {string} lineColor - line color
     * @returns {string}
     * @private
     */
    _makeHorizontalLinesHtml: function(dimension, lineColor) {
        var positions = this._makeVerticalPositions(dimension.height);
        var templateParams = this._makeHorizontalLineTemplateParams({
            width: dimension.width + 'px',
            color: lineColor
        });

        return this._makeLinesHtml(positions, dimension.height, templateParams);
    },

    /**
     * Render plot lines.
     * @param {HTMLElement} container - container element
     * @param {{width: number, height: number}} dimension plot area dimension
     * @private
     */
    _renderPlotLines: function(container, dimension) {
        var lineContainer = dom.create('DIV', 'tui-chart-plot-lines-area');
        var theme = this.theme;
        var lineHtml = '';

        if (!predicate.isLineTypeChart(this.chartType)) {
            lineHtml += this._makeVerticalLinesHtml(dimension, theme.lineColor);
        }

        lineHtml += this._makeHorizontalLinesHtml(dimension, theme.lineColor);

        dom.append(container, lineContainer);
        lineContainer.innerHTML += lineHtml;
        renderUtil.renderBackground(container, theme.background);
    },

    /**
     * Make positions for vertical line.
     * @param {number} height plot height
     * @returns {Array.<number>} positions
     * @private
     */
    _makeVerticalPositions: function(height) {
        var axesData = this.boundsMaker.getAxesData();
        var yAxis = axesData.yAxis || axesData.rightYAxis;
        var positions = calculator.makeTickPixelPositions(height, yAxis.validTickCount);

        positions.shift();

        return positions;
    },

    /**
     * Make divided positions of plot.
     * @param {number} width - plot width
     * @param {number} tickCount - tick count
     * @returns {Array.<number>}
     * @private
     */
    _makeDividedPlotPositions: function(width, tickCount) {
        var yAxisWidth = this.boundsMaker.getDimension('yAxis').width;
        var leftWidth, rightWidth, leftPositions, rightPositions;

        tickCount = parseInt(tickCount / 2, 10) + 1;
        width -= yAxisWidth;
        leftWidth = Math.round((width) / 2);
        rightWidth = width - leftWidth;

        leftPositions = calculator.makeTickPixelPositions(leftWidth, tickCount);
        rightPositions = calculator.makeTickPixelPositions(rightWidth, tickCount, leftWidth + yAxisWidth);

        leftPositions.pop();
        rightPositions.shift();

        return leftPositions.concat(rightPositions);
    },

    /**
     * Make positions for horizontal line.
     * @param {number} width plot width
     * @returns {Array.<number>} positions
     * @private
     */
    _makeHorizontalPositions: function(width) {
        var tickCount = this.boundsMaker.getAxesData().xAxis.validTickCount;
        var positions;

        if (this.options.divided) {
            positions = this._makeDividedPlotPositions(width, tickCount);
        } else {
            positions = calculator.makeTickPixelPositions(width, tickCount);
            positions.shift();
        }

        return positions;
    },

    /**
     * Add plot line.
     * @param {{index: number, color: string, id: string}} data - data
     */
    addPlotLine: function(data) {
        this.options.lines.push(data);
        this.rerender();
    },

    /**
     * Add plot band.
     * @param {{range: [number, number], color: string, id: string}} data - data
     */
    addPlotBand: function(data) {
        this.options.bands.push(data);
        this.rerender();
    },

    /**
     * Remove plot line.
     * @param {string} id - line id
     */
    removePlotLine: function(id) {
        this.options.lines = tui.util.filter(this.options.lines, function(line) {
            return line.id !== id;
        });
        this.rerender();
    },

    /**
     * Remove plot band.
     * @param {string} id - band id
     */
    removePlotBand: function(id) {
        this.options.bands = tui.util.filter(this.options.bands, function(line) {
            return line.id !== id;
        });
        this.rerender();
    },

    /**
     * Animate for adding data.
     * @param {{tickSize: number, shifting: boolean}} data - data for animation
     */
    animateForAddingData: function(data) {
        var self = this;
        var beforeLeft = 0;
        var interval = data.tickSize;
        var areaWidth;

        if (this.dataProcessor.isCoordinateType()) {
            this.optionalContainer.innerHTML = '';
        } else if (data.shifting) {
            renderUtil.startAnimation(300, function(ratio) {
                var left = interval * ratio;
                self.optionalContainer.style.left = (beforeLeft - left) + 'px';
            });
        } else {
            areaWidth = this.boundsMaker.getDimension('plot').width;
            renderUtil.startAnimation(300, function(ratio) {
                var left = interval * ratio;
                self.optionalContainer.style.width = (areaWidth - left) + 'px';
            }, function() {
            });
        }
    }
});

module.exports = Plot;
<|endoftext|>


<commit_before>// ==========================================================================
// rangetouch.js v2.0.0
// Making <input type="range"> work on touch devices
// https://github.com/sampotts/rangetouch
// License: The MIT License (MIT)
// ==========================================================================

import defaults from './config';
import { matches } from './utils/css';
import { trigger } from './utils/events';
import is from './utils/is';
import { round } from './utils/numbers';

class RangeTouch {
    /**
     * Setup a new instance
     * @param {String|Element} target
     * @param {Object} options
     */
    constructor(target, options) {
        if (is.element(target)) {
            // An Element is passed, use it directly
            this.element = target;
        } else if (is.string(target)) {
            // A CSS Selector is passed, fetch it from the DOM
            this.element = document.querySelector(target);
        }

        if (!is.element(this.element) || !is.empty(this.element.rangeTouch)) {
            return;
        }

        this.config = Object.assign({}, defaults, options);

        this.init();
    }

    static get enabled() {
        return 'ontouchstart' in document.documentElement;
    }

    /**
     * Setup multiple instances
     * @param {String|Element|NodeList|Array} target
     * @param {Object} options
     */
    static setup(target, options = {}) {
        let targets = null;

        if (is.empty(target) || is.string(target)) {
            targets = Array.from(document.querySelectorAll(is.string(target) ? target : 'input[type="range"]'));
        } else if (is.element(target)) {
            targets = [target];
        } else if (is.nodeList(target)) {
            targets = Array.from(target);
        } else if (is.array(target)) {
            targets = target.filter(is.element);
        }

        if (is.empty(targets)) {
            return null;
        }

        const config = Object.assign({}, defaults, options);

        if (is.string(target) && config.watch) {
            // Create an observer instance
            const observer = new MutationObserver(mutations => {
                Array.from(mutations).forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(node => {
                        if (!is.element(node) || !matches(node, target)) {
                            return;
                        }

                        // eslint-disable-next-line no-unused-vars
                        const range = new RangeTouch(node, config);
                    });
                });
            });

            // Pass in the target node, as well as the observer options
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
        }

        return targets.map(t => new RangeTouch(t, options));
    }

    init() {
        // Bail if not a touch enabled device
        if (!RangeTouch.enabled) {
            return;
        }

        // Add useful CSS
        if (this.config.addCSS) {
            // TODO: Restore original values on destroy
            this.element.style.userSelect = 'none';
            this.element.style.webKitUserSelect = 'none';
            this.element.style.touchAction = 'manipulation';
        }

        this.listeners(true);

        this.element.rangeTouch = this;
    }

    destroy() {
        // Bail if not a touch enabled device
        if (!RangeTouch.enabled) {
            return;
        }

        this.listeners(false);

        this.element.rangeTouch = null;
    }

    listeners(toggle) {
        const method = toggle ? 'addEventListener' : 'removeEventListener';

        // Listen for events
        ['touchstart', 'touchmove', 'touchend'].forEach(type => {
            this.element[method](type, event => this.set(event), false);
        });
    }

    /**
     * Get the value based on touch position
     * @param {Event} event
     */
    get(event) {
        if (!RangeTouch.enabled || !is.event(event)) {
            return null;
        }

        const input = event.target;
        const touch = event.changedTouches[0];
        const min = parseFloat(input.getAttribute('min')) || 0;
        const max = parseFloat(input.getAttribute('max')) || 100;
        const step = parseFloat(input.getAttribute('step')) || 1;
        const delta = max - min;

        // Calculate percentage
        let percent;
        const clientRect = input.getBoundingClientRect();
        const thumbWidth = ((100 / clientRect.width) * (this.config.thumbWidth / 2)) / 100;

        // Determine left percentage
        percent = (100 / clientRect.width) * (touch.clientX - clientRect.left);

        // Don't allow outside bounds
        if (percent < 0) {
            percent = 0;
        } else if (percent > 100) {
            percent = 100;
        }

        // Factor in the thumb offset
        if (percent < 50) {
            percent -= (100 - percent * 2) * thumbWidth;
        } else if (percent > 50) {
            percent += (percent - 50) * 2 * thumbWidth;
        }

        // Find the closest step to the mouse position
        return min + round(delta * (percent / 100), step);
    }

    /**
     * Update range value based on position
     * @param {Event} event
     */
    set(event) {
        if (!RangeTouch.enabled || !is.event(event) || event.target.disabled) {
            return;
        }

        // Prevent text highlight on iOS
        event.preventDefault();

        // Set value
        event.target.value = this.get(event);

        // Trigger event
        trigger(event.target, event.type === 'touchend' ? 'change' : 'input');
    }
}

export default RangeTouch;
<commit_msg>Removed useful CSS in "destroy" function<commit_after>// ==========================================================================
// rangetouch.js v2.0.0
// Making <input type="range"> work on touch devices
// https://github.com/sampotts/rangetouch
// License: The MIT License (MIT)
// ==========================================================================

import defaults from './config';
import { matches } from './utils/css';
import { trigger } from './utils/events';
import is from './utils/is';
import { round } from './utils/numbers';

class RangeTouch {
    /**
     * Setup a new instance
     * @param {String|Element} target
     * @param {Object} options
     */
    constructor(target, options) {
        if (is.element(target)) {
            // An Element is passed, use it directly
            this.element = target;
        } else if (is.string(target)) {
            // A CSS Selector is passed, fetch it from the DOM
            this.element = document.querySelector(target);
        }

        if (!is.element(this.element) || !is.empty(this.element.rangeTouch)) {
            return;
        }

        this.config = Object.assign({}, defaults, options);

        this.init();
    }

    static get enabled() {
        return 'ontouchstart' in document.documentElement;
    }

    /**
     * Setup multiple instances
     * @param {String|Element|NodeList|Array} target
     * @param {Object} options
     */
    static setup(target, options = {}) {
        let targets = null;

        if (is.empty(target) || is.string(target)) {
            targets = Array.from(document.querySelectorAll(is.string(target) ? target : 'input[type="range"]'));
        } else if (is.element(target)) {
            targets = [target];
        } else if (is.nodeList(target)) {
            targets = Array.from(target);
        } else if (is.array(target)) {
            targets = target.filter(is.element);
        }

        if (is.empty(targets)) {
            return null;
        }

        const config = Object.assign({}, defaults, options);

        if (is.string(target) && config.watch) {
            // Create an observer instance
            const observer = new MutationObserver(mutations => {
                Array.from(mutations).forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(node => {
                        if (!is.element(node) || !matches(node, target)) {
                            return;
                        }

                        // eslint-disable-next-line no-unused-vars
                        const range = new RangeTouch(node, config);
                    });
                });
            });

            // Pass in the target node, as well as the observer options
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
        }

        return targets.map(t => new RangeTouch(t, options));
    }

    init() {
        // Bail if not a touch enabled device
        if (!RangeTouch.enabled) {
            return;
        }

        // Add useful CSS
        if (this.config.addCSS) {
            this.element.style.userSelect = 'none';
            this.element.style.webKitUserSelect = 'none';
            this.element.style.touchAction = 'manipulation';
        }

        this.listeners(true);

        this.element.rangeTouch = this;
    }

    destroy() {
        // Bail if not a touch enabled device
        if (!RangeTouch.enabled) {
            return;
        }

        // Remove useful CSS
        if (this.config.addCSS) {
            this.element.style.userSelect = '';
            this.element.style.webKitUserSelect = '';
            this.element.style.touchAction = '';
        }

        this.listeners(false);

        this.element.rangeTouch = null;
    }

    listeners(toggle) {
        const method = toggle ? 'addEventListener' : 'removeEventListener';

        // Listen for events
        ['touchstart', 'touchmove', 'touchend'].forEach(type => {
            this.element[method](type, event => this.set(event), false);
        });
    }

    /**
     * Get the value based on touch position
     * @param {Event} event
     */
    get(event) {
        if (!RangeTouch.enabled || !is.event(event)) {
            return null;
        }

        const input = event.target;
        const touch = event.changedTouches[0];
        const min = parseFloat(input.getAttribute('min')) || 0;
        const max = parseFloat(input.getAttribute('max')) || 100;
        const step = parseFloat(input.getAttribute('step')) || 1;
        const delta = max - min;

        // Calculate percentage
        let percent;
        const clientRect = input.getBoundingClientRect();
        const thumbWidth = ((100 / clientRect.width) * (this.config.thumbWidth / 2)) / 100;

        // Determine left percentage
        percent = (100 / clientRect.width) * (touch.clientX - clientRect.left);

        // Don't allow outside bounds
        if (percent < 0) {
            percent = 0;
        } else if (percent > 100) {
            percent = 100;
        }

        // Factor in the thumb offset
        if (percent < 50) {
            percent -= (100 - percent * 2) * thumbWidth;
        } else if (percent > 50) {
            percent += (percent - 50) * 2 * thumbWidth;
        }

        // Find the closest step to the mouse position
        return min + round(delta * (percent / 100), step);
    }

    /**
     * Update range value based on position
     * @param {Event} event
     */
    set(event) {
        if (!RangeTouch.enabled || !is.event(event) || event.target.disabled) {
            return;
        }

        // Prevent text highlight on iOS
        event.preventDefault();

        // Set value
        event.target.value = this.get(event);

        // Trigger event
        trigger(event.target, event.type === 'touchend' ? 'change' : 'input');
    }
}

export default RangeTouch;
<|endoftext|>


<commit_before>/**
 * @copyright Devexperts
 */

window.Scrollable = (function(DX, window, document, undefined) {
	'use strict';

	var dom = DX.Dom,
		measure = DX.Measure,
		CN_SCROLLABLE = 'scrollable',
		CN_WRAPPER = CN_SCROLLABLE + '--wrapper',
		CN_CONTAINER = CN_SCROLLABLE + '--container',
		CN_CONTENT = CN_SCROLLABLE + '--content',
		CN_WITH_VERTICAL_SCROLLBAR = CN_SCROLLABLE + '-withVerticalScrollbar',
		CN_WITH_HORIZONTAL_SCROLLBAR = CN_SCROLLABLE + '-withHorizontalScrollbar',
		CN_SCROLLBAR = 'scrollbar',
		CN_SCROLLBAR_VERTICAL = CN_SCROLLBAR + '-vertical',
		CN_SCROLLBAR_HORIZONTAL = CN_SCROLLBAR + '-horizontal',
		CN_BAR = CN_SCROLLBAR + '--bar',
		CN_BUTTON = CN_SCROLLBAR + '--button',
		CN_TRACK = CN_SCROLLBAR + '--track',
		CN_BUTTON_EXTREME = CN_BUTTON + '-extreme',
		CN_BUTTON_FWD = CN_BUTTON + '-forward',
		CN_BUTTON_BACK = CN_BUTTON + '-backward',
		CN_BUTTON_TO_END = CN_BUTTON + '-toEnd',
		CN_BUTTON_TO_START = CN_BUTTON + '-toStart',
		CN_RESIZE_DETECTOR = CN_SCROLLABLE +  '--resizeDetector',
		SCROLL_STEP = 20,
		E_MOUSEWHEEL = (function getMouseWheelEventName() {
			var event = ('onmousewheel' in document.documentElement) ? 'mousewheel' : 'DOMMouseScroll';

			try {
				WheelEvent('wheel');
				event = 'wheel';
			} catch (e) {}

			return event;
		})(),
		ipad = /(iPad|iPhone|iPod)/g.test(navigator.userAgent),
		scrollBarsSize = (function() {
			var testElement = dom.createElement('div', {
					className: CN_CONTAINER
				}),
				verticalScrollSize,
				horizontalScrollSize,
				head = document.getElementsByTagName('head')[0];

			testElement.style.width = '100px';
			testElement.style.height = '100px';
			testElement.style.overflow = 'scroll';
			testElement.style.opacity = 0;

			document.body.appendChild(testElement);
			horizontalScrollSize = testElement.offsetHeight - testElement.clientHeight;
			verticalScrollSize = testElement.offsetWidth - testElement.clientWidth;
			document.body.removeChild(testElement);

			return {
				vertical: verticalScrollSize,
				horizontal: horizontalScrollSize
			};
		})();

	function initElements(scrollableContainer) {
		return {
			scrollable: dom.createElement('div', {
				className: CN_SCROLLABLE
			}),
			wrapper: dom.createElement('div', {
				className: CN_WRAPPER
			}),
			container: scrollableContainer,
			content: dom.createElement('div', {
				className: CN_CONTENT
			}),
			contentResizeDetector: dom.createElement('iframe', {
				className: CN_RESIZE_DETECTOR,
				src: 'about:blank'
			}),
			scrollableResizeDetector: dom.createElement('iframe', {
				className: CN_RESIZE_DETECTOR,
				src: 'about:blank'
			}),
			scrollbarVertical: ipad ? null : initScrollBarElements(CN_SCROLLBAR_VERTICAL),
			scrollbarHorizontal: ipad ? null : initScrollBarElements(CN_SCROLLBAR_HORIZONTAL)
		};
	}

	function initAppearance(elements) {
		fillContentElement(elements);
		makeDefaultStructure(elements);
		addMaxHeight(elements);

		if (!ipad) {
			initScrollbarAppearance(elements.scrollbarVertical, elements.wrapper);
			initScrollbarAppearance(elements.scrollbarHorizontal, elements.wrapper);
			injectResizeDetectors(elements);
		}
	}

	function fillContentElement(elements) {
		var container = elements.container;

		for (var i = 0, total = container.children.length; i < total; i++) {
			elements.content.appendChild(container.children[0]);
		}
	}

	function makeDefaultStructure(elements) {
		var parent = dom.getParent(elements.container);

		parent.insertBefore(elements.scrollable, elements.container);
		elements.wrapper.appendChild(elements.container);
		elements.scrollable.appendChild(elements.wrapper);
		elements.container.appendChild(elements.content);

		transferClassNames(elements);
	}

	function addMaxHeight(elements) {
		var style = window.getComputedStyle(elements.scrollable),
			scrollableMaxHeight =  style.getPropertyValue('max-height'),
				containerMaxHeight;

		if (scrollableMaxHeight !== 'none') {
			scrollableMaxHeight = parseFloat(scrollableMaxHeight);
			containerMaxHeight = scrollableMaxHeight + scrollBarsSize.vertical;

			elements.container.style.maxHeight =  containerMaxHeight + 'px';
		}

	}

	function initScrollBarElements(type) {
		return {
			scrollbar: dom.createElement('div', {
				className: [CN_SCROLLBAR, type]
			}),
			track: dom.createElement('div', {
				className: CN_TRACK
			}),
			bar: dom.createElement('div', {
				className: CN_BAR
			}),
			buttonToEnd: dom.createElement('button', {
				type: 'button',
				className: [CN_BUTTON, CN_BUTTON_EXTREME, CN_BUTTON_TO_END]
			}),
			buttonToStart: dom.createElement('button', {
				type: 'button',
				className: [CN_BUTTON, CN_BUTTON_EXTREME, CN_BUTTON_TO_START]
			}),
			buttonFwd: dom.createElement('button', {
				type: 'button',
				className: [CN_BUTTON, CN_BUTTON_FWD]
			}),
			buttonBack: dom.createElement('button', {
				type: 'button',
				className: [CN_BUTTON, CN_BUTTON_BACK]
			})
		};
	}

	function initScrollbarAppearance(elements, scrollable) {
		var scrollbar = elements.scrollbar;

		scrollbar.appendChild(elements.buttonToStart);
		scrollbar.appendChild(elements.buttonBack);
		scrollbar.appendChild(elements.track);
		scrollbar.appendChild(elements.buttonFwd);
		scrollbar.appendChild(elements.buttonToEnd);
		elements.track.appendChild(elements.bar);

		scrollable.appendChild(scrollbar);
	}

	function injectResizeDetectors(elements) {
		elements.scrollable.appendChild(elements.scrollableResizeDetector);
		elements.content.appendChild(elements.contentResizeDetector);
	}

	function transferClassNames(elements) {
		var scrollable = elements.scrollable,
			container = elements.container;

		scrollable.className = scrollable.className + ' ' + container.className;
		container.className = CN_CONTAINER;
	}

	function isPartOfVerticalScrollbar(element) {
		return dom.getAscendantByClassName(element, CN_SCROLLBAR).classList.contains(CN_SCROLLBAR_VERTICAL);
	}

	function getStyle(element, property) {
		return window.getComputedStyle(element, null).getPropertyValue(property);
	}

	return function Scrollable(scrollableContainer) {
		var elements,
			dragStartY,
			dragStartX,
			dragMove,
			ratio,
			minBarSize = {};

		function init() {
			elements = initElements(scrollableContainer);

			initAppearance(elements);

			if (!ipad) {
				hideSystemScrollbars();
				initListeners();
				updateScrollbars();

				minBarSize = {
					x: parseFloat(getStyle(elements.scrollbarHorizontal.bar, 'min-width')),
					y: parseFloat(getStyle(elements.scrollbarVertical.bar, 'min-height'))
				};
			}
		}

		function hideSystemScrollbars() {
			var container = elements.container;

			container.style.marginRight = -scrollBarsSize.vertical + 'px';
			container.style.marginBottom = -scrollBarsSize.horizontal + 'px';
			container.style.width = 'calc(100% + ' + scrollBarsSize.vertical + 'px)';
			container.style.height = 'calc(100% + ' + scrollBarsSize.horizontal + 'px)';
		}

		function initListeners() {
			initDetectors();
			initUserInteractionListeners();
			fireCreatedEvent();
		}

		function initDetectors() {
			elements.contentResizeDetector.contentWindow.addEventListener(DX.Event.RESIZE, updateScrollbars);
			elements.scrollableResizeDetector.contentWindow.addEventListener(DX.Event.RESIZE, updateScrollbars);
			elements.container.addEventListener(DX.Event.SCROLL, updateBars);
		}

		function initUserInteractionListeners() {
			[elements.scrollbarHorizontal, elements.scrollbarVertical].forEach(function(scrollbar) {
				var track = scrollbar.track;

				track.addEventListener(E_MOUSEWHEEL, scrollbarWheelSpinningHandler, true);
				track.addEventListener(DX.Event.CLICK, scrollbarClickHandler, true);
				scrollbar.buttonFwd.addEventListener(DX.Event.CLICK, btnClickHandler, true);
				scrollbar.buttonBack.addEventListener(DX.Event.CLICK, btnClickHandler, true);
				scrollbar.buttonToEnd.addEventListener(DX.Event.CLICK, extremeBtnClickHandler, true);
				scrollbar.buttonToStart.addEventListener(DX.Event.CLICK, extremeBtnClickHandler, true);
				scrollbar.bar.addEventListener(DX.Event.MOUSE_DOWN, dragStart, true);
			});
		}

		function updateScrollbars() {
			toggleScrollbars();
			updateRatio();
			updateBars();
		}

		function toggleScrollbars() {
			var scrollable = elements.scrollable,
				container = elements.container,
				visibleHeight = container.clientHeight,
				scrollHeight = container.scrollHeight,
				visibleWidth = container.clientWidth,
				scrollWidth = container.scrollWidth;

			scrollable.classList[(scrollHeight > visibleHeight) ? 'add' : 'remove'](CN_WITH_VERTICAL_SCROLLBAR);
			scrollable.classList[(scrollWidth > visibleWidth) ? 'add' : 'remove'](CN_WITH_HORIZONTAL_SCROLLBAR);
		}

		function updateRatio() {
			ratio = {
				x: getAxisRatio('x'),
				y: getAxisRatio('y')
			};
		}

		function updateBars() {
			updateBarsSize();
			updateBarsPosition();
		}

		function getAxisRatio(axis) {
			var container = elements.container,
				isHorizontal = (axis === 'x'),
				property = isHorizontal ? 'Width' : 'Height',
				scrollbar = isHorizontal ? elements.scrollbarHorizontal : elements.scrollbarVertical,
				scrollSize = container['scroll' + property],
				containerSize = container['client' + property],
				trackSize = scrollbar.track['offset' + property],
				sizeRatio =  trackSize / scrollSize,
				possibleBarSize = containerSize * sizeRatio,
				barSize = (possibleBarSize < minBarSize[axis]) ? minBarSize[axis] : possibleBarSize,
				hiddenContentAreaSize = scrollSize - containerSize,
				hiddenTrackAreaSize = trackSize - barSize,
				posRatio = hiddenTrackAreaSize / hiddenContentAreaSize;

			return {
				size: sizeRatio,
				position: posRatio
			};
		}

		function updateBarsSize() {
			var container = elements.container,
				visibleHeight = container.clientHeight,
				visibleWidth = container.clientWidth,
				barHeight = Math.ceil(visibleHeight * ratio.y.size),
				barWidth = Math.ceil(visibleWidth * ratio.x.size);

			elements.scrollbarVertical.bar.style.height = barHeight + 'px';
			elements.scrollbarHorizontal.bar.style.width = barWidth + 'px';
		}

		function updateBarsPosition() {
			var container = elements.container,
				scrollTop = container.scrollTop,
				scrollLeft = container.scrollLeft,
				barPositionTop = Math.floor(scrollTop * ratio.y.position),
				barPositionWidth = Math.floor(scrollLeft * ratio.x.position);

			elements.scrollbarVertical.bar.style.top = barPositionTop + 'px';
			elements.scrollbarHorizontal.bar.style.left = barPositionWidth + 'px';
		}

		function scrollbarWheelSpinningHandler(e) {
			var scrollbar = dom.getAscendantByClassName(e.target, CN_SCROLLBAR),
				container = elements.container,
				scrollLength = isPartOfVerticalScrollbar(scrollbar) ? container.scrollTop : container.scrollLeft;

			if (isPartOfVerticalScrollbar(scrollbar)) {
				container.scrollTop = scrollLength + (e.deltaY * 10 || e.detail * 10 || (e.wheelDelta * -1));
			} else {
				container.scrollLeft = scrollLength + (e.deltaX * 10 || e.detail * 10 || (e.wheelDeltaX * -1));
			}
		}

		function scrollbarClickHandler(e) {
			var isBar = !!dom.getAscendantByClassName(e.target, CN_BAR),
				container = elements.container,
				cursorPosition,
				trackPosition,
				newScrollPosition,
				axis,
				property;

			if (!isBar) {

				if (isPartOfVerticalScrollbar(e.target)) {
					axis = 'y';
					cursorPosition = e.clientY;
					property = 'Top';
				} else {
					axis = 'x';
					cursorPosition = e.clientX;
					property = 'Left';
				}

				trackPosition = measure.getPosition(e.target)[axis];
				newScrollPosition = (cursorPosition - trackPosition) / ratio[axis].position;
				container['scroll' + property] = Math.floor(newScrollPosition);
			}
		}

		function btnClickHandler(e) {
			var direction = e.target.classList.contains(CN_BUTTON_FWD) ? 1 : -1,
				container = elements.container,
				property = isPartOfVerticalScrollbar(e.target) ? 'Top' : 'Left';

			container['scroll' + property] += SCROLL_STEP * direction;
		}

		function extremeBtnClickHandler(e) {
			var isToEnd = e.target.classList.contains(CN_BUTTON_TO_END),
				container = elements.container,
				property = isPartOfVerticalScrollbar(e.target) ? 'Top' : 'Left',
				maxValueProperty = isPartOfVerticalScrollbar(e.target) ? 'Height' : 'Width';

			if (isToEnd) {
				container['scroll' + property] = container['scroll' + maxValueProperty];
			} else {
				container['scroll' + property] = 0;
			}

		}

		function dragStart(e) {
			if (isPartOfVerticalScrollbar(e.target)) {
				dragStartY = e.clientY;
				dragStartX = null;
				dragMove = dragMoveY;
			} else {
				dragStartY = null;
				dragStartX = e.clientX;
				dragMove = dragMoveX;
			}

			document.addEventListener(DX.Event.MOUSE_MOVE, dragMove, true);
			document.addEventListener(DX.Event.MOUSE_UP, dragEnd, true);
			document.addEventListener(DX.Event.SELECT_START, preventSelection, true);
		}

		function dragMoveY(e) {
			var container = elements.container,
				dragDelta = dragStartY - e.clientY,
				scrollTop = container.scrollTop,
				delta = dragDelta / ratio.y.position;

			dragStartY = e.clientY;
			container.scrollTop = scrollTop - delta;
		}

		function dragMoveX(e) {
			var dragDelta = dragStartX - e.clientX,
				scrollLeft = elements.container.scrollLeft,
				delta = dragDelta / ratio.x.position;

			dragStartX = e.clientX;
			elements.container.scrollLeft = scrollLeft - delta;
		}

		function dragEnd(e) {
			dragMove(e);

			document.removeEventListener(DX.Event.MOUSE_MOVE, dragMove, true);
			document.removeEventListener(DX.Event.MOUSE_UP, dragEnd, true);
			document.removeEventListener(DX.Event.SELECT_START, preventSelection, true);
		}

		function preventSelection(e) {
			e.preventDefault();
			return false;
		}

		function getElementContent() {
			return elements.content;
		}

		function fireCreatedEvent() {
			DX.Event.trigger(elements.container, Scrollable.E_CREATED, {
				detail: {
					block: elements.scrollable,
					eventTarget: elements.container,
					elementContent: elements.content
				}
			});
		}

		init();

		this.getElementContent = getElementContent;
	};

})(DX, window, document);

Scrollable.E_CREATED = 'scrollable:created';
<commit_msg>make more isomorphic friendly<commit_after>/**
 * @copyright Devexperts
 */
var Scrollable;

if (typeof window !== 'undefined' && typeof document !== 'undefined') {
	Scrollable = (function(DX, window, document, undefined) {
		'use strict';

		var dom = DX.Dom,
				measure = DX.Measure,
				CN_SCROLLABLE = 'scrollable',
				CN_WRAPPER = CN_SCROLLABLE + '--wrapper',
				CN_CONTAINER = CN_SCROLLABLE + '--container',
				CN_CONTENT = CN_SCROLLABLE + '--content',
				CN_WITH_VERTICAL_SCROLLBAR = CN_SCROLLABLE + '-withVerticalScrollbar',
				CN_WITH_HORIZONTAL_SCROLLBAR = CN_SCROLLABLE + '-withHorizontalScrollbar',
				CN_SCROLLBAR = 'scrollbar',
				CN_SCROLLBAR_VERTICAL = CN_SCROLLBAR + '-vertical',
				CN_SCROLLBAR_HORIZONTAL = CN_SCROLLBAR + '-horizontal',
				CN_BAR = CN_SCROLLBAR + '--bar',
				CN_BUTTON = CN_SCROLLBAR + '--button',
				CN_TRACK = CN_SCROLLBAR + '--track',
				CN_BUTTON_EXTREME = CN_BUTTON + '-extreme',
				CN_BUTTON_FWD = CN_BUTTON + '-forward',
				CN_BUTTON_BACK = CN_BUTTON + '-backward',
				CN_BUTTON_TO_END = CN_BUTTON + '-toEnd',
				CN_BUTTON_TO_START = CN_BUTTON + '-toStart',
				CN_RESIZE_DETECTOR = CN_SCROLLABLE +  '--resizeDetector',
				SCROLL_STEP = 20,
				E_MOUSEWHEEL = (function getMouseWheelEventName() {
					var event = ('onmousewheel' in document.documentElement) ? 'mousewheel' : 'DOMMouseScroll';

					try {
						WheelEvent('wheel');
						event = 'wheel';
					} catch (e) {}

					return event;
				})(),
				ipad = /(iPad|iPhone|iPod)/g.test(navigator.userAgent),
				scrollBarsSize = (function() {
					var testElement = dom.createElement('div', {
								className: CN_CONTAINER
							}),
							verticalScrollSize,
							horizontalScrollSize,
							head = document.getElementsByTagName('head')[0];

					testElement.style.width = '100px';
					testElement.style.height = '100px';
					testElement.style.overflow = 'scroll';
					testElement.style.opacity = 0;

					document.body.appendChild(testElement);
					horizontalScrollSize = testElement.offsetHeight - testElement.clientHeight;
					verticalScrollSize = testElement.offsetWidth - testElement.clientWidth;
					document.body.removeChild(testElement);

					return {
						vertical: verticalScrollSize,
						horizontal: horizontalScrollSize
					};
				})();

		function initElements(scrollableContainer) {
			return {
				scrollable: dom.createElement('div', {
					className: CN_SCROLLABLE
				}),
				wrapper: dom.createElement('div', {
					className: CN_WRAPPER
				}),
				container: scrollableContainer,
				content: dom.createElement('div', {
					className: CN_CONTENT
				}),
				contentResizeDetector: dom.createElement('iframe', {
					className: CN_RESIZE_DETECTOR,
					src: 'about:blank'
				}),
				scrollableResizeDetector: dom.createElement('iframe', {
					className: CN_RESIZE_DETECTOR,
					src: 'about:blank'
				}),
				scrollbarVertical: ipad ? null : initScrollBarElements(CN_SCROLLBAR_VERTICAL),
				scrollbarHorizontal: ipad ? null : initScrollBarElements(CN_SCROLLBAR_HORIZONTAL)
			};
		}

		function initAppearance(elements) {
			fillContentElement(elements);
			makeDefaultStructure(elements);
			addMaxHeight(elements);

			if (!ipad) {
				initScrollbarAppearance(elements.scrollbarVertical, elements.wrapper);
				initScrollbarAppearance(elements.scrollbarHorizontal, elements.wrapper);
				injectResizeDetectors(elements);
			}
		}

		function fillContentElement(elements) {
			var container = elements.container;

			for (var i = 0, total = container.children.length; i < total; i++) {
				elements.content.appendChild(container.children[0]);
			}
		}

		function makeDefaultStructure(elements) {
			var parent = dom.getParent(elements.container);

			parent.insertBefore(elements.scrollable, elements.container);
			elements.wrapper.appendChild(elements.container);
			elements.scrollable.appendChild(elements.wrapper);
			elements.container.appendChild(elements.content);

			transferClassNames(elements);
		}

		function addMaxHeight(elements) {
			var style = window.getComputedStyle(elements.scrollable),
					scrollableMaxHeight =  style.getPropertyValue('max-height'),
					containerMaxHeight;

			if (scrollableMaxHeight !== 'none') {
				scrollableMaxHeight = parseFloat(scrollableMaxHeight);
				containerMaxHeight = scrollableMaxHeight + scrollBarsSize.vertical;

				elements.container.style.maxHeight =  containerMaxHeight + 'px';
			}

		}

		function initScrollBarElements(type) {
			return {
				scrollbar: dom.createElement('div', {
					className: [CN_SCROLLBAR, type]
				}),
				track: dom.createElement('div', {
					className: CN_TRACK
				}),
				bar: dom.createElement('div', {
					className: CN_BAR
				}),
				buttonToEnd: dom.createElement('button', {
					type: 'button',
					className: [CN_BUTTON, CN_BUTTON_EXTREME, CN_BUTTON_TO_END]
				}),
				buttonToStart: dom.createElement('button', {
					type: 'button',
					className: [CN_BUTTON, CN_BUTTON_EXTREME, CN_BUTTON_TO_START]
				}),
				buttonFwd: dom.createElement('button', {
					type: 'button',
					className: [CN_BUTTON, CN_BUTTON_FWD]
				}),
				buttonBack: dom.createElement('button', {
					type: 'button',
					className: [CN_BUTTON, CN_BUTTON_BACK]
				})
			};
		}

		function initScrollbarAppearance(elements, scrollable) {
			var scrollbar = elements.scrollbar;

			scrollbar.appendChild(elements.buttonToStart);
			scrollbar.appendChild(elements.buttonBack);
			scrollbar.appendChild(elements.track);
			scrollbar.appendChild(elements.buttonFwd);
			scrollbar.appendChild(elements.buttonToEnd);
			elements.track.appendChild(elements.bar);

			scrollable.appendChild(scrollbar);
		}

		function injectResizeDetectors(elements) {
			elements.scrollable.appendChild(elements.scrollableResizeDetector);
			elements.content.appendChild(elements.contentResizeDetector);
		}

		function transferClassNames(elements) {
			var scrollable = elements.scrollable,
					container = elements.container;

			scrollable.className = scrollable.className + ' ' + container.className;
			container.className = CN_CONTAINER;
		}

		function isPartOfVerticalScrollbar(element) {
			return dom.getAscendantByClassName(element, CN_SCROLLBAR).classList.contains(CN_SCROLLBAR_VERTICAL);
		}

		function getStyle(element, property) {
			return window.getComputedStyle(element, null).getPropertyValue(property);
		}

		return function Scrollable(scrollableContainer) {
			var elements,
					dragStartY,
					dragStartX,
					dragMove,
					ratio,
					minBarSize = {};

			function init() {
				elements = initElements(scrollableContainer);

				initAppearance(elements);

				if (!ipad) {
					hideSystemScrollbars();
					initListeners();
					updateScrollbars();

					minBarSize = {
						x: parseFloat(getStyle(elements.scrollbarHorizontal.bar, 'min-width')),
						y: parseFloat(getStyle(elements.scrollbarVertical.bar, 'min-height'))
					};
				}
			}

			function hideSystemScrollbars() {
				var container = elements.container;

				container.style.marginRight = -scrollBarsSize.vertical + 'px';
				container.style.marginBottom = -scrollBarsSize.horizontal + 'px';
				container.style.width = 'calc(100% + ' + scrollBarsSize.vertical + 'px)';
				container.style.height = 'calc(100% + ' + scrollBarsSize.horizontal + 'px)';
			}

			function initListeners() {
				initDetectors();
				initUserInteractionListeners();
				fireCreatedEvent();
			}

			function initDetectors() {
				elements.contentResizeDetector.contentWindow.addEventListener(DX.Event.RESIZE, updateScrollbars);
				elements.scrollableResizeDetector.contentWindow.addEventListener(DX.Event.RESIZE, updateScrollbars);
				elements.container.addEventListener(DX.Event.SCROLL, updateBars);
			}

			function initUserInteractionListeners() {
				[elements.scrollbarHorizontal, elements.scrollbarVertical].forEach(function(scrollbar) {
					var track = scrollbar.track;

					track.addEventListener(E_MOUSEWHEEL, scrollbarWheelSpinningHandler, true);
					track.addEventListener(DX.Event.CLICK, scrollbarClickHandler, true);
					scrollbar.buttonFwd.addEventListener(DX.Event.CLICK, btnClickHandler, true);
					scrollbar.buttonBack.addEventListener(DX.Event.CLICK, btnClickHandler, true);
					scrollbar.buttonToEnd.addEventListener(DX.Event.CLICK, extremeBtnClickHandler, true);
					scrollbar.buttonToStart.addEventListener(DX.Event.CLICK, extremeBtnClickHandler, true);
					scrollbar.bar.addEventListener(DX.Event.MOUSE_DOWN, dragStart, true);
				});
			}

			function updateScrollbars() {
				toggleScrollbars();
				updateRatio();
				updateBars();
			}

			function toggleScrollbars() {
				var scrollable = elements.scrollable,
						container = elements.container,
						visibleHeight = container.clientHeight,
						scrollHeight = container.scrollHeight,
						visibleWidth = container.clientWidth,
						scrollWidth = container.scrollWidth;

				scrollable.classList[(scrollHeight > visibleHeight) ? 'add' : 'remove'](CN_WITH_VERTICAL_SCROLLBAR);
				scrollable.classList[(scrollWidth > visibleWidth) ? 'add' : 'remove'](CN_WITH_HORIZONTAL_SCROLLBAR);
			}

			function updateRatio() {
				ratio = {
					x: getAxisRatio('x'),
					y: getAxisRatio('y')
				};
			}

			function updateBars() {
				updateBarsSize();
				updateBarsPosition();
			}

			function getAxisRatio(axis) {
				var container = elements.container,
						isHorizontal = (axis === 'x'),
						property = isHorizontal ? 'Width' : 'Height',
						scrollbar = isHorizontal ? elements.scrollbarHorizontal : elements.scrollbarVertical,
						scrollSize = container['scroll' + property],
						containerSize = container['client' + property],
						trackSize = scrollbar.track['offset' + property],
						sizeRatio =  trackSize / scrollSize,
						possibleBarSize = containerSize * sizeRatio,
						barSize = (possibleBarSize < minBarSize[axis]) ? minBarSize[axis] : possibleBarSize,
						hiddenContentAreaSize = scrollSize - containerSize,
						hiddenTrackAreaSize = trackSize - barSize,
						posRatio = hiddenTrackAreaSize / hiddenContentAreaSize;

				return {
					size: sizeRatio,
					position: posRatio
				};
			}

			function updateBarsSize() {
				var container = elements.container,
						visibleHeight = container.clientHeight,
						visibleWidth = container.clientWidth,
						barHeight = Math.ceil(visibleHeight * ratio.y.size),
						barWidth = Math.ceil(visibleWidth * ratio.x.size);

				elements.scrollbarVertical.bar.style.height = barHeight + 'px';
				elements.scrollbarHorizontal.bar.style.width = barWidth + 'px';
			}

			function updateBarsPosition() {
				var container = elements.container,
						scrollTop = container.scrollTop,
						scrollLeft = container.scrollLeft,
						barPositionTop = Math.floor(scrollTop * ratio.y.position),
						barPositionWidth = Math.floor(scrollLeft * ratio.x.position);

				elements.scrollbarVertical.bar.style.top = barPositionTop + 'px';
				elements.scrollbarHorizontal.bar.style.left = barPositionWidth + 'px';
			}

			function scrollbarWheelSpinningHandler(e) {
				var scrollbar = dom.getAscendantByClassName(e.target, CN_SCROLLBAR),
						container = elements.container,
						scrollLength = isPartOfVerticalScrollbar(scrollbar) ? container.scrollTop : container.scrollLeft;

				if (isPartOfVerticalScrollbar(scrollbar)) {
					container.scrollTop = scrollLength + (e.deltaY * 10 || e.detail * 10 || (e.wheelDelta * -1));
				} else {
					container.scrollLeft = scrollLength + (e.deltaX * 10 || e.detail * 10 || (e.wheelDeltaX * -1));
				}
			}

			function scrollbarClickHandler(e) {
				var isBar = !!dom.getAscendantByClassName(e.target, CN_BAR),
						container = elements.container,
						cursorPosition,
						trackPosition,
						newScrollPosition,
						axis,
						property;

				if (!isBar) {

					if (isPartOfVerticalScrollbar(e.target)) {
						axis = 'y';
						cursorPosition = e.clientY;
						property = 'Top';
					} else {
						axis = 'x';
						cursorPosition = e.clientX;
						property = 'Left';
					}

					trackPosition = measure.getPosition(e.target)[axis];
					newScrollPosition = (cursorPosition - trackPosition) / ratio[axis].position;
					container['scroll' + property] = Math.floor(newScrollPosition);
				}
			}

			function btnClickHandler(e) {
				var direction = e.target.classList.contains(CN_BUTTON_FWD) ? 1 : -1,
						container = elements.container,
						property = isPartOfVerticalScrollbar(e.target) ? 'Top' : 'Left';

				container['scroll' + property] += SCROLL_STEP * direction;
			}

			function extremeBtnClickHandler(e) {
				var isToEnd = e.target.classList.contains(CN_BUTTON_TO_END),
						container = elements.container,
						property = isPartOfVerticalScrollbar(e.target) ? 'Top' : 'Left',
						maxValueProperty = isPartOfVerticalScrollbar(e.target) ? 'Height' : 'Width';

				if (isToEnd) {
					container['scroll' + property] = container['scroll' + maxValueProperty];
				} else {
					container['scroll' + property] = 0;
				}

			}

			function dragStart(e) {
				if (isPartOfVerticalScrollbar(e.target)) {
					dragStartY = e.clientY;
					dragStartX = null;
					dragMove = dragMoveY;
				} else {
					dragStartY = null;
					dragStartX = e.clientX;
					dragMove = dragMoveX;
				}

				document.addEventListener(DX.Event.MOUSE_MOVE, dragMove, true);
				document.addEventListener(DX.Event.MOUSE_UP, dragEnd, true);
				document.addEventListener(DX.Event.SELECT_START, preventSelection, true);
			}

			function dragMoveY(e) {
				var container = elements.container,
						dragDelta = dragStartY - e.clientY,
						scrollTop = container.scrollTop,
						delta = dragDelta / ratio.y.position;

				dragStartY = e.clientY;
				container.scrollTop = scrollTop - delta;
			}

			function dragMoveX(e) {
				var dragDelta = dragStartX - e.clientX,
						scrollLeft = elements.container.scrollLeft,
						delta = dragDelta / ratio.x.position;

				dragStartX = e.clientX;
				elements.container.scrollLeft = scrollLeft - delta;
			}

			function dragEnd(e) {
				dragMove(e);

				document.removeEventListener(DX.Event.MOUSE_MOVE, dragMove, true);
				document.removeEventListener(DX.Event.MOUSE_UP, dragEnd, true);
				document.removeEventListener(DX.Event.SELECT_START, preventSelection, true);
			}

			function preventSelection(e) {
				e.preventDefault();
				return false;
			}

			function getElementContent() {
				return elements.content;
			}

			function fireCreatedEvent() {
				DX.Event.trigger(elements.container, Scrollable.E_CREATED, {
					detail: {
						block: elements.scrollable,
						eventTarget: elements.container,
						elementContent: elements.content
					}
				});
			}

			init();

			this.getElementContent = getElementContent;
		};

	})(DX, window, document);

	Scrollable.E_CREATED = 'scrollable:created';

	window.Scrollable = Scrollable;
}

<|endoftext|>