<commit_before>(function () {

    var root = this;

    var defaults = {
        chart: {
            animations: {
                enable: true,
                // duration of the animation in ms
                duration: 400,
            },
            // by default take the size of the parent container
            defaultWidth: 400,
            // height = width * ratio
            defaultAspect: 1 / 1.61803398875,
            // calculated at render time based on the options & container
            width: undefined,
            // if defined, height takes precedence over aspect
            height: undefined,
            // margin between the container and the chart (ie labels or axis title)
            margin: {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            },
            // padding between the chart area and the inner plot area */
            padding: {
                top: null,
                right: null,
                bottom: null,
                left: null
            },
            internalPadding: {
                bottom: 0,
                left: 0
            },
            // automatically false by default anyway; adding here to help generate docs
            rotatedFrame: false,
            // width in pixels of the plot area (area inside the axis if any). This gets calculated on render
            plotWidth: undefined,
            // height in pixels of the plot area (area inside the axis if any). This gets calculated on render
            plotHeight: undefined,
            // top edge in pixels (from the edge of the svg) of the plot area (area inside the axis if any). This gets calculated on render
            plotTop: undefined,
            // left edge in pixels (from the edge of the svg) of the plot area (area inside the axis if any). This gets calculated on render
            plotLeft: undefined,
        },

        xAxis: {
        },

        yAxis: {
        },

        tooltip: {
        }
    };

    /**
    * Creates a Contour instance, based on the core Contour object. This instance can contain a set of related visualizations.
    *
    *   * Pass the constructor any configuration options in the *options* parameter. Make sure the `el` option contains the selector of the container in which the Contour instance will be rendered.
    *   * Set the frame for this Contour instance (e.g. `.cartesian()`).
    *   * Add one or more specific visualizations to this Contour instance (e.g. `.scatter()`, `.trend-line()`). Pass each visualization constructor the data it displays. Pass configuration options if desired.
    *   * Invoke an action for this Contour instance (e.g. `.render()`).
    *
    * ### Example:
    *
    *     new Contour({el: 'myChart'})
    *       .cartesian()
    *       .line([1,3,2,5])
    *       .render()
    *
    *
    * @class Contour() visualizations object
    * @param {object} options The global configuration options object
    *
    */
    function Contour (options) {
        this.init(options);

        return this;
    }

    /**
    * Adds a new kind of visualization to the core Contour object.
    * The *renderer* function is called when you add this visualization to instances of Contour.
    *
    * ### Example:
    *
    *     Contour.export("exampleVisualization", function(data, layer) {
    *           //function body to create exampleVisualization
    *           //for example using SVG and/or D3
    *     });
    *
    *     //to include the visualization into a specific Contour instance
    *     new Contour(options)
    *           .exampleVisualization(data)
    *           .render()
    *
    * @param {String} ctorName Name of the visualization, used as a constructor name.
    * @param {Function} renderer Function called when this visualization is added to a Contour instance. This function receives the data that is passed in to the constructor.
    */
    Contour.export = function (ctorName, renderer) {

        if (typeof renderer !== 'function') throw new Error('Invalid render function for ' + ctorName + ' visualization');

        function sortSeries(data) {
            if(!data || !data.length) return [];

            if(data[0].data) {
                _.each(data, sortSeries);
            }

            var shouldSort = _.isObject(data[0]) && _.isDate(data[0].x);
            var sortFunc = function (a, b) { return a.x - b.x; };
            if(shouldSort) {
                data.sort(sortFunc);
            }

            return data;
        }

        Contour.prototype[ctorName] = function (data, options) {
            var categories = this.options ? this.options.xAxis ? this.options.xAxis.categories : undefined : undefined;
            var opt =  _.extend({}, this.options[ctorName], options);
            var vis;

            data = data || [];
            sortSeries(data);
            vis = new Contour.VisualizationContainer(_.nw.normalizeSeries(data, categories), opt, ctorName, renderer, this);
            this._visualizations.push(vis);

            return this;
        };
    };

    /**
    * Exposes functionality to the core Contour object.
    * Use this to add *functionality* that will be available for any visualizations.
    *
    * ###Example:
    *
    *     Contour.expose("example", {
    *          // when included in the instance, the function `.myFunction` is available in the visualizations
    *         myFunction: function(data) { .... }
    *     });
    *
    *     Contour.export("visualizationThatUsesMyFunction", function(data, layer) {
    *           //function body including call to this.myFunction(data)
    *     });
    *
    *     // to include the functionality into a specific instance
    *     new Contour(options)
    *           .example()
    *           .visualizationThatUsesMyFunction()
    *           .render()
    */
    Contour.expose = function (ctorName, functionalityConstructor) {
        var ctor = function () {
            var functionality = typeof functionalityConstructor === 'function' ? new functionalityConstructor() : functionalityConstructor;
            // extend the --instance-- we don't want all charts to be overriden...
            _.extend(this, _.omit(functionality, 'init'));

            if(functionality.init) functionality.init.call(this, this.options);

            return this;
        };

        Contour.prototype[ctorName] = ctor;

        return this;
    };

    Contour.prototype = _.extend(Contour.prototype, {
        _visualizations: undefined,

        _extraOptions: undefined,

        // Initializes the instance of Narwhal
        init: function (options) {
            // for now, just  store this options here...
            // the final set of options will be composed before rendering
            // after all components/visualizations have been added
            this.options = options || {};

            this._extraOptions = [];
            this._visualizations = [];

            return this;
        },

        calculateWidth: function () {
            var width = _.nw.getStyle(this.options.el, 'width');
            return this.options.el ? (parseInt(width, 10) || this.options.chart.defaultWidth) : this.options.chart.defaultWidth;
        },

        calculateHeight: function () {
            var height = _.nw.getStyle(this.options.el, 'height');
            var containerHeight = this.options.el ? parseInt(height, 10) : undefined;
            var calcWidth = this.options.chart.width;
            var ratio = this.options.chart.aspect || this.options.chart.defaultAspect;

            return !!containerHeight && containerHeight > 1 ?  containerHeight : Math.round(calcWidth * ratio);
        },

        calcMetrics: function () {
            var options = this.options;

            this.adjustPadding();

            this.adjustTitlePadding();

            options.chart.width = options.chart.width || this.calculateWidth();
            options.chart.height = options.chart.height || this.calculateHeight();

            this.options = _.merge(options, {
                chart: {
                    plotWidth: options.chart.width - options.chart.margin.left - options.chart.margin.right - options.chart.internalPadding.left - options.chart.padding.right,
                    plotHeight: options.chart.height - options.chart.margin.top - options.chart.margin.bottom - options.chart.padding.top - options.chart.internalPadding.bottom,
                    plotLeft: options.chart.margin.left + options.chart.internalPadding.left,
                    plotTop: options.chart.margin.top + options.chart.padding.top
                }
            });
        },

        adjustPadding: function () {
            // overriden by components that need to adjust padding
            return this;
        },

        adjustTitlePadding: function () {
            // overriden by components that need to adjust padding
            return this;
        },

        composeOptions: function () {
            var allDefaults = _.merge({}, defaults);
            var mergeExtraOptions = function (opt) { _.merge(allDefaults, opt); };
            var mergeDefaults = function (vis) { _.merge(allDefaults, vis.renderer.defaults); };

            _.each(this._extraOptions, mergeExtraOptions);
            _.each(this._visualizations, mergeDefaults);

            // compose the final list of options right before start rendering
            this.options = _.merge({}, allDefaults, this.options);
        },

        baseRender: function () {
            this.plotArea();

            return this;
        },

        /**
        * Renders this Contour instance and all its visualizations into the DOM.
        *
        * Example:
        *
        *     new Contour({ el:'.myChart' })
        *           .pie([1,2,3])
        *           .render()
        *
        * @function render
        *
        */
        render: function () {
            this.composeOptions();

            this.calcMetrics();

            this.baseRender();

            this.renderVisualizations();

            return this;
        },

        update: function () {
            this.calcMetrics();
            return this;
        },

        plotArea: function () {

            var chartOpt = this.options.chart;

            this.container = d3.select(this.options.el);
            // fix a flicker im web-kit when animating opacity and the chart is in an iframe
            this.container.attr('style', '-webkit-backface-visibility: hidden;');

            if(!this.svg) {
                this.svg = this.container
                    .append('svg')
                    .attr('viewBox', '0 0 ' + chartOpt.width + ' ' + chartOpt.height)
                    .attr('preserveAspectRatio', 'xMinYMin')
                    .attr('class', 'contour-chart')
                    .attr('height', chartOpt.height)
                    .append('g')
                        .attr('transform', 'translate(' + chartOpt.margin.left + ',' + chartOpt.margin.top + ')');
            }

            return this;
        },

        createVisualizationLayer: function (vis, id) {
            return this.svg.append('g')
                .attr('vis-id', id)
                .attr('vis-type', vis.type)
                .attr('transform', 'translate(' + this.options.chart.internalPadding.left + ',' + this.options.chart.padding.top + ')');
        },

        renderVisualizations: function () {

            _.each(this._visualizations, function (visualization, index) {
                var id = index + 1;
                var layer = visualization.layer || this.createVisualizationLayer(visualization, id);
                var opt = _.merge({}, this.options, visualization.options);
                visualization.layer = layer;
                visualization.parent = this;
                visualization.render(layer, opt, this);
            }, this);

            return this;
        },

        /**
        * Sets the same data into all visualizations for a Contour instance. Useful for creating interactive
        * visualizations: call after getting the additional data from the user.
        *
        * ###Example:
        *
        *     var data = [1,2,3,4,5];
        *     var chart = new Contour({ el:'.myChart' })
        *           .cartesian()
        *           .scatter(data)
        *           .trendLine(data);
        *
        *     data.push(10);
        *     chart.setData(data)
        *           .render();
        *
        * @function setData
        *
        */
        setData: function (data) {
            _.invoke(this._visualizations, 'setData', data);

            return this;
        },

        /**
        * Returns a VisualizationContainer object for the visualization at a given index (0-based).
        *
        * ###Example:
        *
        *     var chart = new Contour({ el:'.myChart' })
        *           .pie([1,2,3])
        *           .render()
        *
        *     var myPie = chart.select(0)
        *
        *     // do something with the visualization, for example updating its data set
        *     myPie.setData([6,7,8,9]).render()
        *
        * @function select
        *
        */
        select: function (index) {
            return this._visualizations[index];
        },

        // place holder function for now
        data: function () {

        }
    });

    // export to our context
    root.Contour = Contour;

})();
<commit_msg>Added facility to check if some functionality (a dependency) is included into the instance<commit_after>(function () {

    var root = this;

    var defaults = {
        chart: {
            animations: {
                enable: true,
                // duration of the animation in ms
                duration: 400,
            },
            // by default take the size of the parent container
            defaultWidth: 400,
            // height = width * ratio
            defaultAspect: 1 / 1.61803398875,
            // calculated at render time based on the options & container
            width: undefined,
            // if defined, height takes precedence over aspect
            height: undefined,
            // margin between the container and the chart (ie labels or axis title)
            margin: {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            },
            // padding between the chart area and the inner plot area */
            padding: {
                top: null,
                right: null,
                bottom: null,
                left: null
            },
            internalPadding: {
                bottom: 0,
                left: 0
            },
            // automatically false by default anyway; adding here to help generate docs
            rotatedFrame: false,
            // width in pixels of the plot area (area inside the axis if any). This gets calculated on render
            plotWidth: undefined,
            // height in pixels of the plot area (area inside the axis if any). This gets calculated on render
            plotHeight: undefined,
            // top edge in pixels (from the edge of the svg) of the plot area (area inside the axis if any). This gets calculated on render
            plotTop: undefined,
            // left edge in pixels (from the edge of the svg) of the plot area (area inside the axis if any). This gets calculated on render
            plotLeft: undefined,
        },

        xAxis: {
        },

        yAxis: {
        },

        tooltip: {
        }
    };

    /**
    * Creates a Contour instance, based on the core Contour object. This instance can contain a set of related visualizations.
    *
    *   * Pass the constructor any configuration options in the *options* parameter. Make sure the `el` option contains the selector of the container in which the Contour instance will be rendered.
    *   * Set the frame for this Contour instance (e.g. `.cartesian()`).
    *   * Add one or more specific visualizations to this Contour instance (e.g. `.scatter()`, `.trend-line()`). Pass each visualization constructor the data it displays. Pass configuration options if desired.
    *   * Invoke an action for this Contour instance (e.g. `.render()`).
    *
    * ### Example:
    *
    *     new Contour({el: 'myChart'})
    *       .cartesian()
    *       .line([1,3,2,5])
    *       .render()
    *
    *
    * @class Contour() visualizations object
    * @param {object} options The global configuration options object
    *
    */
    function Contour (options) {
        this.init(options);

        return this;
    }

    /**
    * Adds a new kind of visualization to the core Contour object.
    * The *renderer* function is called when you add this visualization to instances of Contour.
    *
    * ### Example:
    *
    *     Contour.export("exampleVisualization", function(data, layer) {
    *           //function body to create exampleVisualization
    *           //for example using SVG and/or D3
    *     });
    *
    *     //to include the visualization into a specific Contour instance
    *     new Contour(options)
    *           .exampleVisualization(data)
    *           .render()
    *
    * @param {String} ctorName Name of the visualization, used as a constructor name.
    * @param {Function} renderer Function called when this visualization is added to a Contour instance. This function receives the data that is passed in to the constructor.
    */
    Contour.export = function (ctorName, renderer) {

        if (typeof renderer !== 'function') throw new Error('Invalid render function for ' + ctorName + ' visualization');

        function sortSeries(data) {
            if(!data || !data.length) return [];

            if(data[0].data) {
                _.each(data, sortSeries);
            }

            var shouldSort = _.isObject(data[0]) && _.isDate(data[0].x);
            var sortFunc = function (a, b) { return a.x - b.x; };
            if(shouldSort) {
                data.sort(sortFunc);
            }

            return data;
        }

        Contour.prototype[ctorName] = function (data, options) {
            var categories = this.options ? this.options.xAxis ? this.options.xAxis.categories : undefined : undefined;
            var opt =  _.extend({}, this.options[ctorName], options);
            var vis;

            data = data || [];
            sortSeries(data);
            vis = new Contour.VisualizationContainer(_.nw.normalizeSeries(data, categories), opt, ctorName, renderer, this);
            this._visualizations.push(vis);

            return this;
        };
    };

    /**
    * Exposes functionality to the core Contour object.
    * Use this to add *functionality* that will be available for any visualizations.
    *
    * ###Example:
    *
    *     Contour.expose("example", {
    *          // when included in the instance, the function `.myFunction` is available in the visualizations
    *         myFunction: function(data) { .... }
    *     });
    *
    *     Contour.export("visualizationThatUsesMyFunction", function(data, layer) {
    *           //function body including call to this.myFunction(data)
    *     });
    *
    *     // to include the functionality into a specific instance
    *     new Contour(options)
    *           .example()
    *           .visualizationThatUsesMyFunction()
    *           .render()
    */
    Contour.expose = function (ctorName, functionalityConstructor) {
        var ctor = function () {

            var functionality = typeof functionalityConstructor === 'function' ? new functionalityConstructor() : functionalityConstructor;
            // extend the --instance-- we don't want all charts to be overriden...
            _.extend(this, _.omit(functionality, 'init'));

            if(functionality.init) functionality.init.call(this, this.options);

            // keep a list of the included functionality into this instance
            // so we can match and check dependencies
            this._exposed.push(ctorName);

            return this;
        };

        Contour.prototype[ctorName] = ctor;

        return this;
    };

    Contour.prototype = _.extend(Contour.prototype, {
        _visualizations: undefined,

        _extraOptions: undefined,

        _exposed: undefined,

        // Initializes the instance of Narwhal
        init: function (options) {
            // for now, just  store this options here...
            // the final set of options will be composed before rendering
            // after all components/visualizations have been added
            this.options = options || {};

            this._extraOptions = [];
            this._visualizations = [];
            this._exposed = [];

            return this;
        },

        calculateWidth: function () {
            var width = _.nw.getStyle(this.options.el, 'width');
            return this.options.el ? (parseInt(width, 10) || this.options.chart.defaultWidth) : this.options.chart.defaultWidth;
        },

        calculateHeight: function () {
            var height = _.nw.getStyle(this.options.el, 'height');
            var containerHeight = this.options.el ? parseInt(height, 10) : undefined;
            var calcWidth = this.options.chart.width;
            var ratio = this.options.chart.aspect || this.options.chart.defaultAspect;

            return !!containerHeight && containerHeight > 1 ?  containerHeight : Math.round(calcWidth * ratio);
        },

        calcMetrics: function () {
            var options = this.options;

            this.adjustPadding();

            this.adjustTitlePadding();

            options.chart.width = options.chart.width || this.calculateWidth();
            options.chart.height = options.chart.height || this.calculateHeight();

            this.options = _.merge(options, {
                chart: {
                    plotWidth: options.chart.width - options.chart.margin.left - options.chart.margin.right - options.chart.internalPadding.left - options.chart.padding.right,
                    plotHeight: options.chart.height - options.chart.margin.top - options.chart.margin.bottom - options.chart.padding.top - options.chart.internalPadding.bottom,
                    plotLeft: options.chart.margin.left + options.chart.internalPadding.left,
                    plotTop: options.chart.margin.top + options.chart.padding.top
                }
            });
        },

        adjustPadding: function () {
            // overriden by components that need to adjust padding
            return this;
        },

        adjustTitlePadding: function () {
            // overriden by components that need to adjust padding
            return this;
        },

        composeOptions: function () {
            var allDefaults = _.merge({}, defaults);
            var mergeExtraOptions = function (opt) { _.merge(allDefaults, opt); };
            var mergeDefaults = function (vis) { _.merge(allDefaults, vis.renderer.defaults); };

            _.each(this._extraOptions, mergeExtraOptions);
            _.each(this._visualizations, mergeDefaults);

            // compose the final list of options right before start rendering
            this.options = _.merge({}, allDefaults, this.options);
        },

        baseRender: function () {
            this.plotArea();

            return this;
        },

        /**
        * Renders this Contour instance and all its visualizations into the DOM.
        *
        * Example:
        *
        *     new Contour({ el:'.myChart' })
        *           .pie([1,2,3])
        *           .render()
        *
        * @function render
        *
        */
        render: function () {
            this.composeOptions();

            this.calcMetrics();

            this.baseRender();

            this.renderVisualizations();

            return this;
        },

        update: function () {
            this.calcMetrics();
            return this;
        },

        plotArea: function () {

            var chartOpt = this.options.chart;

            this.container = d3.select(this.options.el);
            // fix a flicker im web-kit when animating opacity and the chart is in an iframe
            this.container.attr('style', '-webkit-backface-visibility: hidden;');

            if(!this.svg) {
                this.svg = this.container
                    .append('svg')
                    .attr('viewBox', '0 0 ' + chartOpt.width + ' ' + chartOpt.height)
                    .attr('preserveAspectRatio', 'xMinYMin')
                    .attr('class', 'contour-chart')
                    .attr('height', chartOpt.height)
                    .append('g')
                        .attr('transform', 'translate(' + chartOpt.margin.left + ',' + chartOpt.margin.top + ')');
            }

            return this;
        },

        createVisualizationLayer: function (vis, id) {
            return this.svg.append('g')
                .attr('vis-id', id)
                .attr('vis-type', vis.type)
                .attr('transform', 'translate(' + this.options.chart.internalPadding.left + ',' + this.options.chart.padding.top + ')');
        },

        renderVisualizations: function () {

            _.each(this._visualizations, function (visualization, index) {
                var id = index + 1;
                var layer = visualization.layer || this.createVisualizationLayer(visualization, id);
                var opt = _.merge({}, this.options, visualization.options);
                visualization.layer = layer;
                visualization.parent = this;
                visualization.render(layer, opt, this);
            }, this);

            return this;
        },

        /**
        * Assert that all the dependencies are into the Contour instance
        * for example if a visualization requires Cartisian to be included in the instance
        * it would call this.checkDependencies('Cartesian'), and the framework would
        * give a helpful error message if it was not included
        *
        * @function checkDependencies
        * @param {string|array} list of dependencies (as specified in the instance constructor)
        *
        */
        checkDependencies: function (listOfDependencies) {
            listOfDependencies = _.isArray(listOfDependencies) ? listOfDependencies : [listOfDependencies];
            var _this = this;
            var missing = [];

            _.each(listOfDependencies, function (dep) {
                if (_this._exposed.indexOf(dep) === -1) {
                    missing.push(dep);
                }
            });

            if (missing.length) {
                throw new Error('Missing depeendencies in the Contour instance (ej. new Contour({}).cartesian()): [' + missing.join(', ') + ']');
            }
        },

        /**
        * Sets the same data into all visualizations for a Contour instance. Useful for creating interactive
        * visualizations: call after getting the additional data from the user.
        *
        * ###Example:
        *
        *     var data = [1,2,3,4,5];
        *     var chart = new Contour({ el:'.myChart' })
        *           .cartesian()
        *           .scatter(data)
        *           .trendLine(data);
        *
        *     data.push(10);
        *     chart.setData(data)
        *           .render();
        *
        * @function setData
        *
        */
        setData: function (data) {
            _.invoke(this._visualizations, 'setData', data);

            return this;
        },

        /**
        * Returns a VisualizationContainer object for the visualization at a given index (0-based).
        *
        * ###Example:
        *
        *     var chart = new Contour({ el:'.myChart' })
        *           .pie([1,2,3])
        *           .render()
        *
        *     var myPie = chart.select(0)
        *
        *     // do something with the visualization, for example updating its data set
        *     myPie.setData([6,7,8,9]).render()
        *
        * @function select
        *
        */
        select: function (index) {
            return this._visualizations[index];
        },

        // place holder function for now
        data: function () {

        }
    });

    // export to our context
    root.Contour = Contour;

})();
<|endoftext|>


<commit_before>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require) {
    'use strict';

    var EventTypes = require('wf-js-uicomponents/awesome_map/EventTypes');
    var DestroyUtil = require('wf-js-common/DestroyUtil');

    var DEFAULT_MIN_SIZE = 16;
    var DEFAULT_SCROLLER_Z_INDEX = 3;
    var DEFAULT_SCROLLER_CLASS = 'scroller';
    var DEFAULT_VERT_SCROLLER_CLASS = 'vertical-scroller';
    var DEFAULT_HORIZ_SCROLLER_CLASS = 'horizontal-scroller';
    var DEFAULT_TRACK_CLASS = 'scroll-track';
    var DEFAULT_VERT_TRACK_CLASS = 'vertical-scroll-track';
    var DEFAULT_HORIZ_TRACK_CLASS = 'horizontal-scroll-track';

    /**
     * Creates a new ScrollBar with the given ScrollList and options.
     *
     * @classdesc
     *
     * A ScrollBar provides a UI based on the traditional browser scrollbar
     * for users to scroll the ScrollList.
     *
     * @name ScrollBar
     * @constructor
     *
     * @param {ScrollList} scrollList
     *        The ScrollList that the ScrollBar is constructed for
     *
     * @param {HTMLElement} parent
     *        The parent element for the ScrollBar to be created within
     *
     * @param {Object} options
     *        An object with optional classes and ids to be placed on the
     *        scrollerEl and scrollTrackEl.
     *
     * @param {number} options.minSize
     *        The minimum height or width, in pixels, of the ScrollBar
     *        If not provided, the minSize will default to 16
     *
     * @param {string} options.orientation
     *        The orientation of the scrollbar, can be either 'horizontal'
     *        or 'vertical'. Defaults to 'vertical'.
     *
     * @param {number} options.trackMargin
     *        An amount (in px) to subtract from the end of the scroll track.
     *        Can be used to keep horizontal and vertical scroll bars from
     *        crossing.
     *
     * @param {number} options.scrollerThickness
     *        The thickness of the actual scroller. This will be width for a
     *        vertical scrollbar and height for a horizontal one.
     *
     * @param {number} options.scrollTrackThickness
     *        The thickness of the scroll track. Usually the same as scroller
     *        thickness. Width for a vertical scrollbar and height for a
     *        horizontal one.
     *
     * @param {number} options.scrollBarOffset
     *        The offset from the "natural" position of the scrollbar. The
     *        natural position is the far right for a vertical scrollbar and
     *        the bottom for a horizontal one.
     *
     * @param {number} options.scrollerZIndex
     *        The z-index to apply to the scroller.
     *
     *
     * @example
     *
     * var parent = getElementById('parent');
     * var options = {};
     *
     * options.scrollerId = "scrollbar";
     * options.scrollerClass = "scrollbar";
     * options.scrollTrackId = "scrollbar-container";
     * options.scrollTrackClass = "scrollbar-container";
     * options.minSize = 8;
     * options.orientation = 'vertical';
     *
     * var scrollBar = new ScrollBar(scrollList, parent, options);
     */

    var ScrollBar = function(scrollList, parent, options) {
        var self = this;

        if (!scrollList) {
            throw new Error('ScrollBar#ScrollBar: scrollList is required');
        }
        else {
            this._scrollList = scrollList;
        }

        if (!parent) {
            throw new Error('ScrollBar#ScrollBar: parent is required.');
        }
        else {
            this._parent = parent;
        }

        // This is the currently active map, it can be either a list map, or an
        // item map, depending on the scroll mode.
        this._activeMap = this._scrollList.getListMap();

        this._options = options;
        this._trackMargin = this._options.trackMargin || 0;
        this._isVertical = this._options.orientation === 'horizontal' ? false : true;
        this._scrollerPosition = 0.0;
        this._scrollerScrolling = false;
        this._resize = false;
        this._disposed = false;

        this._setupDOM();
        this._placeScroller();

        // Callbacks

        // Make adjustments when the user zooms
        var mapScaleChangedHandler = function() {
            self._cacheDimensions();
            self._setTrackSize();
            self._setScrollerSize();
            self._placeScroller();
        };

        // Match the scroll bar to the document position
        var mapTranslationChangedHandler = function(sender, offset) {
            if (self._isVertical) {
                self._extent = -offset.y;
            } else {
                self._extent = -offset.x;
            }

            if (!self._scrollerScrolling && !self._disposed) {
                self._placeScroller();
            }
        };

        // Bind map callbacks

        function updateMap() {
            var itemMap = self._scrollList.getCurrentItemMap();
            if (itemMap) {
                // We are in single or peek mode
                self._activeMap = itemMap;
                self._cacheDimensions();
                self._placeScroller();
            }
        }

        function bindHandlers() {
            self._activeMap.onScaleChanged(mapScaleChangedHandler);
            self._activeMap.onTranslationChanged(mapTranslationChangedHandler);
        }

        function unbindHandlers() {
            self._activeMap.onScaleChanged.remove(mapScaleChangedHandler);
            self._activeMap.onTranslationChanged.remove(mapTranslationChangedHandler);
        }

        updateMap();
        bindHandlers();

        this._scrollList.onCurrentItemChanged(function changeItemMap() {
            unbindHandlers();
            updateMap();
            bindHandlers();
        });

        // Bind scroll list callbacks

        // Adjust sizes and bar when items are inserted
        this._scrollList.onItemsInserted(function() {
            self._cacheDimensions();
            self._placeScroller();
        });

        // Make adjustments when the scrollList is resized
        this._scrollList.onInteraction(function(scrollList, args) {
            if (args.event.type === EventTypes.RESIZE) {
                self._resize = true;
            }

            if (args.event.type === EventTypes.RELEASE && self._resize === true) {
                self._cacheDimensions();
                self._setTrackSize();
                self._setScrollerSize();
                self._resize = false;
            }
        });

        // If part of the page is highlighted and the user clicks and drags on the page,
        // a 'drag' event may be triggered instead of the standard mousedown-mouseup event.
        // This results in mouseup never firing, and the mousemove handler never being unbound.
        // Preventing default on dragstarts on the scroller prevents this from occuring.
        this._elements.scroller.addEventListener('dragstart', function(event) {
            event.preventDefault();
        });

        this._mouseupHandler = function() {
            // _stopUpdatingScrollbar unbinds the 'mousemove' and 'mouseup' handlers from the document
            self._stopUpdatingScrollbar();
        };

        this._mousemoveHandler = function(e) {
            var coord;
            if (self._isVertical) {
                coord = e.clientY;
            } else {
                coord = e.clientX;
            }
            self._updateScroller(coord);
        };

        // Attach handlers for scrolling the ScrollBar
        this._mousedownHandler = function(e) {
            // Prevent the event from bubbling so that we don't end up with
            // weird selections all over the page.
            e.preventDefault();

            // _mouseupHandler will ensure that, in the event that the mousemove event is not caught,
            // the event handlers will be unbound before being bound again.
            self._mouseupHandler();

            var coord;
            var barOffset;
            if (self._isVertical) {
                coord = e.clientY;
                barOffset = self._elements.scroller.offsetTop;
            } else {
                coord = e.clientX;
                barOffset = self._elements.scroller.offsetLeft;
            }

            self._clickOffset = coord - barOffset;
            self._scrollerScrolling = true;

            document.addEventListener('mousemove', self._mousemoveHandler);
            document.addEventListener('mouseup', self._mouseupHandler);
        };

        this._elements.scroller.addEventListener('mousedown', this._mousedownHandler);
    };

    ScrollBar.prototype = {

        //---------------------------------------------------------
        // Public methods
        //---------------------------------------------------------

        /**
         * Find out whether the scrollbar is vertical.
         * @return bool
         */
        isVertical: function() {
            return this._isVertical;
        },

        /**
         * Destruct the ScrollBar created HTML elements
         * @method ScrollBar#dispose
         */
        dispose: function() {
            this._elements.scroller.removeEventListener('mousedown', this._mousedownHandler);
            this._elements.scrollTrack.removeChild(this._elements.scroller);
            this._parent.removeChild(this._elements.scrollTrack);
            DestroyUtil.destroy(this);
            this._disposed = true;
        },

        //---------------------------------------------------------
        // Private methods
        //---------------------------------------------------------

        /**
         * Set the position of the scroller to the given value. The value
         * should be in [0.0, 1.0], where 0.0 is the top/left and 1.0 is the
         * bottom/right. The value will be clamped if it is out of range. If no
         * value is provided, the currently set value will be used.
         *
         * @method ScrollBar#setScrollerPosition
         * @param value {number}
         */
        _setScrollerPosition: function(value) {
            if (value === undefined) {
                value = this._scrollerPosition;
            }

            if (value < 0.0) {
                value = 0.0;
            }
            if (value > 1.0) {
                value = 1.0;
            }

            this._scrollerPosition = value;

            var barOffset = Math.round(this._scrollerPosition * (this._trackSize - this._scrollerSize));
            if (this._isVertical) {
                this._elements.scroller.style.top = barOffset + 'px';
            } else {
                this._elements.scroller.style.left = barOffset + 'px';
            }
        },

        /**
         * Calculate the scroll bar height based on the viewport height and the scaled virtual height
         */
        _setScrollerSize: function() {
            var minSize = this._options.minSize || DEFAULT_MIN_SIZE;
            var scrollerSize = Math.max(
                    minSize,
                    (this._viewportSize / this._contentSize * this._viewportSize));
            if (scrollerSize >= this._viewportSize) {
                scrollerSize = 0;
            }

            this._scrollerSize = scrollerSize;

            if (this._isVertical) {
                this._elements.scroller.style.height = scrollerSize + 'px';
            } else {
                this._elements.scroller.style.width = scrollerSize + 'px';
            }
        },

        /**
         * Compute and set the track size to the size of the viewport in the
         * appropriate dimension.
         *
         * @method ScrollBar#setTrackSize
         */
        _setTrackSize: function() {
            var trackSize = this._viewportSize - this._trackMargin;
            this._trackSize = trackSize;

            if (this._isVertical) {
                this._elements.scrollTrack.style.height = trackSize + 'px';
            } else {
                this._elements.scrollTrack.style.width = trackSize + 'px';
            }
        },

        /**
         * Initialize the HTML elements used by this instance of ScrollBar
         * @private
         */
        _setupDOM: function() {
            var scrollerEl = document.createElement('div');
            if (this._options.scrollerId) {
                scrollerEl.setAttribute('id', this._options.scrollerId);
            }
            if (this._options.scrollerClass) {
                scrollerEl.className = this._options.scrollerClass;
            } else {
                scrollerEl.className = DEFAULT_SCROLLER_CLASS + ' ' +
                    (this._isVertical ? DEFAULT_VERT_SCROLLER_CLASS : DEFAULT_HORIZ_SCROLLER_CLASS);
            }

            var scrollTrackEl = document.createElement('div');
            if (this._options.scrollTrackId) {
                scrollTrackEl.setAttribute('id', this._options.scrollTrackId);
            }
            if (this._options.scrollTrackClass) {
                scrollTrackEl.className = this._options.scrollTrackClass;
            } else {
                scrollTrackEl.className = DEFAULT_TRACK_CLASS + ' ' +
                    (this._isVertical ? DEFAULT_VERT_TRACK_CLASS : DEFAULT_HORIZ_TRACK_CLASS);
            }

            var scrollerThickness = this._options.scrollerThickness || '';
            var scrollTrackThickness = this._options.scrollTrackThickness || '';
            var scrollBarOffset = this._options.scrollBarOffset || 0;

            if (this._isVertical) {
                scrollerEl.style.width = scrollerThickness + 'px';
                scrollTrackEl.style.width = scrollTrackThickness + 'px';
                scrollTrackEl.style.right = scrollBarOffset + 'px';
            } else {
                scrollerEl.style.height = scrollerThickness + 'px';
                scrollTrackEl.style.height = scrollTrackThickness + 'px';
                scrollTrackEl.style.bottom = scrollBarOffset + 'px';
            }

            scrollerEl.style.position = 'absolute';
            scrollerEl.style.zIndex = this._options.scrollerZIndex || DEFAULT_SCROLLER_Z_INDEX;

            scrollTrackEl.style.position = 'absolute';

            // Append the scroller and its scroll track to the given
            // parent element.
            scrollTrackEl.appendChild(scrollerEl);
            this._parent.appendChild(scrollTrackEl);

            this._elements = { scroller: scrollerEl, scrollTrack: scrollTrackEl };

            // Compute scale and size attributes
            this._cacheDimensions();

            this._setTrackSize();
            this._setScrollerSize();
        },

        /**
         * Position the scroller based on the current position of the ScrollList
         */
        _placeScroller: function() {
            var scrollerPosition = this._extent / this._virtualSize;
            this._setScrollerPosition(scrollerPosition);
        },

        /**
         * Attempt to move the scroller to a particular location, move it to
         * the closest valid position. The input is either an x or y coordinate
         * depending on whether the scrollbar is horizontal (x) or vertical
         * (y).
         */
        _updateScroller: function(coord) {
            // Clamp to the beginning of the scroll track
            var scrollerCoord = Math.max(0, coord - this._clickOffset);
            // Clamp to the end of the scroll track
            scrollerCoord = Math.min(scrollerCoord, this._trackSize - this._scrollerSize);

            var scrollerPosition = scrollerCoord / (this._trackSize - this._scrollerSize);
            this._setScrollerPosition(scrollerPosition);

            // Use the ratio of the scroller position to find the current
            // item in the list map.
            var itemPosition = scrollerPosition * this._virtualSize;

            var transformState = this._activeMap.getCurrentTransformState();
            var x, y;
            if (this._isVertical) {
                x = transformState.translateX;
                y = -itemPosition;
            } else {
                x = -itemPosition;
                y = transformState.translateY;
            }

            this._activeMap.transform({
                x: x,
                y: y,
                scale: this._scale
            });
            this._scrollList.render();
        },

        /**
         * Remove the event listeners that update the scrollbar from the document
         */
        _stopUpdatingScrollbar: function() {
            this._clickOffset = undefined;
            this._scrollerScrolling = false;
            document.removeEventListener('mousemove', this._mousemoveHandler);
            document.removeEventListener('mouseup', this._mouseupHandler);
        },

        /**
         * Scale the virtual height and re-calculate the scroll bar height
         */
        _cacheDimensions: function() {
            var transformState = this._activeMap.getCurrentTransformState();
            var contentSize = this._activeMap.getContentDimensions();
            var viewportSize = this._activeMap.getViewportDimensions();

            this._scale = transformState.scale;
            if (this._isVertical) {
                this._extent = -transformState.translateY;
                this._viewportSize = viewportSize.height;
                this._contentSize = contentSize.height * this._scale;
                this._virtualSize = this._contentSize - this._viewportSize;
            } else {
                this._extent = -transformState.translateX;
                this._viewportSize = viewportSize.width;
                this._contentSize = contentSize.width * this._scale;
                this._virtualSize = this._contentSize - this._viewportSize;
            }
        }

    };

    return ScrollBar;

});
<commit_msg>Ensure scroller size is being adjusted.<commit_after>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require) {
    'use strict';

    var EventTypes = require('wf-js-uicomponents/awesome_map/EventTypes');
    var DestroyUtil = require('wf-js-common/DestroyUtil');

    var DEFAULT_MIN_SIZE = 16;
    var DEFAULT_SCROLLER_Z_INDEX = 3;
    var DEFAULT_SCROLLER_CLASS = 'scroller';
    var DEFAULT_VERT_SCROLLER_CLASS = 'vertical-scroller';
    var DEFAULT_HORIZ_SCROLLER_CLASS = 'horizontal-scroller';
    var DEFAULT_TRACK_CLASS = 'scroll-track';
    var DEFAULT_VERT_TRACK_CLASS = 'vertical-scroll-track';
    var DEFAULT_HORIZ_TRACK_CLASS = 'horizontal-scroll-track';

    /**
     * Creates a new ScrollBar with the given ScrollList and options.
     *
     * @classdesc
     *
     * A ScrollBar provides a UI based on the traditional browser scrollbar
     * for users to scroll the ScrollList.
     *
     * @name ScrollBar
     * @constructor
     *
     * @param {ScrollList} scrollList
     *        The ScrollList that the ScrollBar is constructed for
     *
     * @param {HTMLElement} parent
     *        The parent element for the ScrollBar to be created within
     *
     * @param {Object} options
     *        An object with optional classes and ids to be placed on the
     *        scrollerEl and scrollTrackEl.
     *
     * @param {number} options.minSize
     *        The minimum height or width, in pixels, of the ScrollBar
     *        If not provided, the minSize will default to 16
     *
     * @param {string} options.orientation
     *        The orientation of the scrollbar, can be either 'horizontal'
     *        or 'vertical'. Defaults to 'vertical'.
     *
     * @param {number} options.trackMargin
     *        An amount (in px) to subtract from the end of the scroll track.
     *        Can be used to keep horizontal and vertical scroll bars from
     *        crossing.
     *
     * @param {number} options.scrollerThickness
     *        The thickness of the actual scroller. This will be width for a
     *        vertical scrollbar and height for a horizontal one.
     *
     * @param {number} options.scrollTrackThickness
     *        The thickness of the scroll track. Usually the same as scroller
     *        thickness. Width for a vertical scrollbar and height for a
     *        horizontal one.
     *
     * @param {number} options.scrollBarOffset
     *        The offset from the "natural" position of the scrollbar. The
     *        natural position is the far right for a vertical scrollbar and
     *        the bottom for a horizontal one.
     *
     * @param {number} options.scrollerZIndex
     *        The z-index to apply to the scroller.
     *
     *
     * @example
     *
     * var parent = getElementById('parent');
     * var options = {};
     *
     * options.scrollerId = "scrollbar";
     * options.scrollerClass = "scrollbar";
     * options.scrollTrackId = "scrollbar-container";
     * options.scrollTrackClass = "scrollbar-container";
     * options.minSize = 8;
     * options.orientation = 'vertical';
     *
     * var scrollBar = new ScrollBar(scrollList, parent, options);
     */

    var ScrollBar = function(scrollList, parent, options) {
        var self = this;

        if (!scrollList) {
            throw new Error('ScrollBar#ScrollBar: scrollList is required');
        }
        else {
            this._scrollList = scrollList;
        }

        if (!parent) {
            throw new Error('ScrollBar#ScrollBar: parent is required.');
        }
        else {
            this._parent = parent;
        }

        // This is the currently active map, it can be either a list map, or an
        // item map, depending on the scroll mode.
        this._activeMap = this._scrollList.getListMap();

        this._options = options;
        this._trackMargin = this._options.trackMargin || 0;
        this._isVertical = this._options.orientation === 'horizontal' ? false : true;
        this._scrollerPosition = 0.0;
        this._scrollerScrolling = false;
        this._resize = false;
        this._disposed = false;

        this._setupDOM();
        this._placeScroller();

        // Callbacks

        // Make adjustments when the user zooms
        var mapScaleChangedHandler = function() {
            self._cacheDimensions();
            self._placeScroller();
        };

        // Match the scroll bar to the document position
        var mapTranslationChangedHandler = function(sender, offset) {
            if (self._isVertical) {
                self._extent = -offset.y;
            } else {
                self._extent = -offset.x;
            }

            if (!self._scrollerScrolling && !self._disposed) {
                self._placeScroller();
            }
        };

        // Bind map callbacks

        function updateMap() {
            var itemMap = self._scrollList.getCurrentItemMap();
            if (itemMap) {
                // We are in single or peek mode
                self._activeMap = itemMap;
                self._cacheDimensions();
                self._placeScroller();
            }
        }

        function bindHandlers() {
            self._activeMap.onScaleChanged(mapScaleChangedHandler);
            self._activeMap.onTranslationChanged(mapTranslationChangedHandler);
        }

        function unbindHandlers() {
            self._activeMap.onScaleChanged.remove(mapScaleChangedHandler);
            self._activeMap.onTranslationChanged.remove(mapTranslationChangedHandler);
        }

        updateMap();
        bindHandlers();

        this._scrollList.onCurrentItemChanged(function changeItemMap() {
            unbindHandlers();
            updateMap();
            bindHandlers();
        });

        // Bind scroll list callbacks

        // Adjust sizes and bar when items are inserted
        this._scrollList.onItemsInserted(function() {
            self._cacheDimensions();
            self._placeScroller();
        });

        // Make adjustments when the scrollList is resized
        this._scrollList.onInteraction(function(scrollList, args) {
            if (args.event.type === EventTypes.RESIZE) {
                self._resize = true;
            }

            if (args.event.type === EventTypes.RELEASE && self._resize === true) {
                self._cacheDimensions();
                self._resize = false;
            }
        });

        // If part of the page is highlighted and the user clicks and drags on the page,
        // a 'drag' event may be triggered instead of the standard mousedown-mouseup event.
        // This results in mouseup never firing, and the mousemove handler never being unbound.
        // Preventing default on dragstarts on the scroller prevents this from occuring.
        this._elements.scroller.addEventListener('dragstart', function(event) {
            event.preventDefault();
        });

        this._mouseupHandler = function() {
            // _stopUpdatingScrollbar unbinds the 'mousemove' and 'mouseup' handlers from the document
            self._stopUpdatingScrollbar();
        };

        this._mousemoveHandler = function(e) {
            var coord;
            if (self._isVertical) {
                coord = e.clientY;
            } else {
                coord = e.clientX;
            }
            self._updateScroller(coord);
        };

        // Attach handlers for scrolling the ScrollBar
        this._mousedownHandler = function(e) {
            // Prevent the event from bubbling so that we don't end up with
            // weird selections all over the page.
            e.preventDefault();

            // _mouseupHandler will ensure that, in the event that the mousemove event is not caught,
            // the event handlers will be unbound before being bound again.
            self._mouseupHandler();

            var coord;
            var barOffset;
            if (self._isVertical) {
                coord = e.clientY;
                barOffset = self._elements.scroller.offsetTop;
            } else {
                coord = e.clientX;
                barOffset = self._elements.scroller.offsetLeft;
            }

            self._clickOffset = coord - barOffset;
            self._scrollerScrolling = true;

            document.addEventListener('mousemove', self._mousemoveHandler);
            document.addEventListener('mouseup', self._mouseupHandler);
        };

        this._elements.scroller.addEventListener('mousedown', this._mousedownHandler);
    };

    ScrollBar.prototype = {

        //---------------------------------------------------------
        // Public methods
        //---------------------------------------------------------

        /**
         * Find out whether the scrollbar is vertical.
         * @return bool
         */
        isVertical: function() {
            return this._isVertical;
        },

        /**
         * Destruct the ScrollBar created HTML elements
         * @method ScrollBar#dispose
         */
        dispose: function() {
            this._elements.scroller.removeEventListener('mousedown', this._mousedownHandler);
            this._elements.scrollTrack.removeChild(this._elements.scroller);
            this._parent.removeChild(this._elements.scrollTrack);
            DestroyUtil.destroy(this);
            this._disposed = true;
        },

        //---------------------------------------------------------
        // Private methods
        //---------------------------------------------------------

        /**
         * Set the position of the scroller to the given value. The value
         * should be in [0.0, 1.0], where 0.0 is the top/left and 1.0 is the
         * bottom/right. The value will be clamped if it is out of range. If no
         * value is provided, the currently set value will be used.
         *
         * @method ScrollBar#setScrollerPosition
         * @param value {number}
         */
        _setScrollerPosition: function(value) {
            if (value === undefined) {
                value = this._scrollerPosition;
            }

            if (value < 0.0) {
                value = 0.0;
            }
            if (value > 1.0) {
                value = 1.0;
            }

            this._scrollerPosition = value;

            var barOffset = Math.round(this._scrollerPosition * (this._trackSize - this._scrollerSize));
            if (this._isVertical) {
                this._elements.scroller.style.top = barOffset + 'px';
            } else {
                this._elements.scroller.style.left = barOffset + 'px';
            }
        },

        /**
         * Calculate the scroll bar height based on the viewport height and the scaled virtual height
         */
        _setScrollerSize: function() {
            var minSize = this._options.minSize || DEFAULT_MIN_SIZE;
            var scrollerSize = Math.max(
                    minSize,
                    (this._viewportSize / this._contentSize * this._viewportSize));
            if (scrollerSize >= this._viewportSize) {
                scrollerSize = 0;
            }

            this._scrollerSize = scrollerSize;

            if (this._isVertical) {
                this._elements.scroller.style.height = scrollerSize + 'px';
            } else {
                this._elements.scroller.style.width = scrollerSize + 'px';
            }
        },

        /**
         * Compute and set the track size to the size of the viewport in the
         * appropriate dimension.
         *
         * @method ScrollBar#setTrackSize
         */
        _setTrackSize: function() {
            var trackSize = this._viewportSize - this._trackMargin;
            this._trackSize = trackSize;

            if (this._isVertical) {
                this._elements.scrollTrack.style.height = trackSize + 'px';
            } else {
                this._elements.scrollTrack.style.width = trackSize + 'px';
            }
        },

        /**
         * Initialize the HTML elements used by this instance of ScrollBar
         * @private
         */
        _setupDOM: function() {
            var scrollerEl = document.createElement('div');
            if (this._options.scrollerId) {
                scrollerEl.setAttribute('id', this._options.scrollerId);
            }
            if (this._options.scrollerClass) {
                scrollerEl.className = this._options.scrollerClass;
            } else {
                scrollerEl.className = DEFAULT_SCROLLER_CLASS + ' ' +
                    (this._isVertical ? DEFAULT_VERT_SCROLLER_CLASS : DEFAULT_HORIZ_SCROLLER_CLASS);
            }

            var scrollTrackEl = document.createElement('div');
            if (this._options.scrollTrackId) {
                scrollTrackEl.setAttribute('id', this._options.scrollTrackId);
            }
            if (this._options.scrollTrackClass) {
                scrollTrackEl.className = this._options.scrollTrackClass;
            } else {
                scrollTrackEl.className = DEFAULT_TRACK_CLASS + ' ' +
                    (this._isVertical ? DEFAULT_VERT_TRACK_CLASS : DEFAULT_HORIZ_TRACK_CLASS);
            }

            var scrollerThickness = this._options.scrollerThickness || '';
            var scrollTrackThickness = this._options.scrollTrackThickness || '';
            var scrollBarOffset = this._options.scrollBarOffset || 0;

            if (this._isVertical) {
                scrollerEl.style.width = scrollerThickness + 'px';
                scrollTrackEl.style.width = scrollTrackThickness + 'px';
                scrollTrackEl.style.right = scrollBarOffset + 'px';
            } else {
                scrollerEl.style.height = scrollerThickness + 'px';
                scrollTrackEl.style.height = scrollTrackThickness + 'px';
                scrollTrackEl.style.bottom = scrollBarOffset + 'px';
            }

            scrollerEl.style.position = 'absolute';
            scrollerEl.style.zIndex = this._options.scrollerZIndex || DEFAULT_SCROLLER_Z_INDEX;

            scrollTrackEl.style.position = 'absolute';

            // Append the scroller and its scroll track to the given
            // parent element.
            scrollTrackEl.appendChild(scrollerEl);
            this._parent.appendChild(scrollTrackEl);

            this._elements = { scroller: scrollerEl, scrollTrack: scrollTrackEl };

            // Compute scale and size attributes
            this._cacheDimensions();
        },

        /**
         * Position the scroller based on the current position of the ScrollList
         */
        _placeScroller: function() {
            var scrollerPosition = this._extent / this._virtualSize;
            this._setScrollerPosition(scrollerPosition);
        },

        /**
         * Attempt to move the scroller to a particular location, move it to
         * the closest valid position. The input is either an x or y coordinate
         * depending on whether the scrollbar is horizontal (x) or vertical
         * (y).
         */
        _updateScroller: function(coord) {
            // Clamp to the beginning of the scroll track
            var scrollerCoord = Math.max(0, coord - this._clickOffset);
            // Clamp to the end of the scroll track
            scrollerCoord = Math.min(scrollerCoord, this._trackSize - this._scrollerSize);

            var scrollerPosition = scrollerCoord / (this._trackSize - this._scrollerSize);
            this._setScrollerPosition(scrollerPosition);

            // Use the ratio of the scroller position to find the current
            // item in the list map.
            var itemPosition = scrollerPosition * this._virtualSize;

            var transformState = this._activeMap.getCurrentTransformState();
            var x, y;
            if (this._isVertical) {
                x = transformState.translateX;
                y = -itemPosition;
            } else {
                x = -itemPosition;
                y = transformState.translateY;
            }

            this._activeMap.transform({
                x: x,
                y: y,
                scale: this._scale
            });
            this._scrollList.render();
        },

        /**
         * Remove the event listeners that update the scrollbar from the document
         */
        _stopUpdatingScrollbar: function() {
            this._clickOffset = undefined;
            this._scrollerScrolling = false;
            document.removeEventListener('mousemove', this._mousemoveHandler);
            document.removeEventListener('mouseup', this._mouseupHandler);
        },

        /**
         * Scale the virtual height and re-calculate the scroll bar height
         */
        _cacheDimensions: function() {
            var transformState = this._activeMap.getCurrentTransformState();
            var contentSize = this._activeMap.getContentDimensions();
            var viewportSize = this._activeMap.getViewportDimensions();

            this._scale = transformState.scale;
            if (this._isVertical) {
                this._extent = -transformState.translateY;
                this._viewportSize = viewportSize.height;
                this._contentSize = contentSize.height * this._scale;
                this._virtualSize = this._contentSize - this._viewportSize;
            } else {
                this._extent = -transformState.translateX;
                this._viewportSize = viewportSize.width;
                this._contentSize = contentSize.width * this._scale;
                this._virtualSize = this._contentSize - this._viewportSize;
            }

            this._setTrackSize();
            this._setScrollerSize();
        }

    };

    return ScrollBar;

});
<|endoftext|>