Show:
/* Copyright © 2015-2016 David Valdman */

define(function(require, exports, module){
    var View = require('samsara/core/View');
    var Surface = require('samsara/dom/Surface');
    var Transform = require('samsara/core/Transform');
    var Stream = require('samsara/streams/Stream');
    var Transitionable = require('samsara/core/Transitionable');

    var Differential = require('samsara/streams/Differential');
    var Accumulator = require('samsara/streams/Accumulator');

    var MouseInput = require('samsara/inputs/MouseInput');
    var TouchInput = require('samsara/inputs/TouchInput');
    var GenericInput = require('samsara/inputs/GenericInput');

    GenericInput.register({
        mouse : MouseInput,
        touch : TouchInput
    });

    /**
     * A UI element that creates a slider controllable by mouse and touch events.
     *  A starting value and range is provided, and the user can change the value within
     *  the range by dragging and clicking on the slider.
     *  The slider has a `.value` property that defines its value as a stream.
     *
     *  This file comes with an associated CSS file slider.css
     *
     *  @example
     *
     *      var slider = new Slider({
     *          value : 90,
     *          range : [0, 360],
     *          label : 'angle'
     *      });
     *
     *      var rotation = slider.value.map(function(angle){
     *          return Transform.rotate(Math.PI * angle / 180);
     *      });
     *
     *      context.add({transform : rotation}).add(surface);
     *      context.add(slider);
     *
     * @class Slider
     * @extends View
     * @namespace UI
     * @constructor
     * @param [options] {Object}                    Options
     * @param [options.value=0.5] {Number}          Starting value
     * @param [options.range=[0,1]] {Array}         Range of values ([min, max])
     * @param [options.label] {String}              Name of label
     * @param [options.precision=1] {Number}        Number of decimal points to display
     * @param [options.transition=false] {Object}   Default transition to animate values
     */
    var Slider = View.extend({
        defaults : {
            value : 0.5,
            range : [0, 1],
            label : '',
            precision : 1,
            transition : false
        },
        initialize : function(options){
            setupSurfaces.call(this, options);
            setupState.call(this, options);
            setupEvents.call(this, options);
            setupRenderTree.call(this, options);

            this.size = Stream.lift(function(size, labelSize){
                if (!labelSize) return false;
                return [size[0], size[1] + labelSize[1]];
            }, [this.foreground.size, this.label.size]);
        },
        /**
         * Set a new end value with an optional transition.
         *  An optional callback can fire when the transition completes.
         *
         * @method set
         * @param value {Number|Number[]}           End value
         * @param [transition] {Object}             Transition definition
         * @param [callback] {Function}             Callback
         */
        set : function set(value, transition, callback){
            var ratio = value2ratio.call(this, value);
            if (transition === undefined) transition = this.options.transition;
            if (transition){
                this.transition.reset(this.ratio.get());
                this.transition.set(ratio, transition, callback);
            }
            else this.ratio.set(ratio);
        },
        /**
         * Return the current value of the slider.
         *
         * @method get
         * @return {Number|Number[]}    Current state
         */
        get : function get(){
            return ratio2value.call(this, this.ratio.get());
        }
    });

    function value2ratio(value){
        var min = this.options.range[0];
        var max = this.options.range[1];
        return (value - min) / (max - min);
    }

    function ratio2value(ratio){
        var min = this.options.range[0];
        var max = this.options.range[1];
        return min + ratio * (max - min);
    }

    function setupSurfaces(options){
        this.background = new Surface({
            classes : ['samsara-slider-background']
        });

        this.foreground = new Surface({
            classes : ['samsara-slider-foreground']
        });

        var template = String(
            '<span class="label">' + options.label +
                '<span class="range">' + '[' + options.range[0] + '|' + options.range[1] + ']</span>' +
            '</span>' +
            '<span class="value"></span>'
        );

        this.label = new Surface({
            size : [undefined, true],
            classes : ['samsara-slider-label'],
            content : template
        });

        this.label.on('deploy', function(target){
            this.labelContent = target.querySelector('.value');
        }.bind(this));
    }

    function setupState(options){
        var initRatio = value2ratio.call(this, options.value);
        this.ratio = new Accumulator(initRatio, {min : 0, max : 1});

        this.transition = new Transitionable(initRatio);
        this.transitionDelta = new Differential();
        this.transitionDelta.subscribe(this.transition);

        this.value = this.ratio.map(ratio2value.bind(this));
    }

    function setupRenderTree(options){
        var foregroundTransform = this.ratio.map(function(value){
            return Transform.scaleX(value);
        });

        var labelTransform = this.background.size.map(function(size){
            return Transform.translateY(size[1]);
        });

        // Render tree
        this.add(this.background);
        this.add({transform : foregroundTransform})
            .add(this.foreground);
        this.add({transform : labelTransform})
            .add(this.label);
    }

    function setupEvents(options){
        // Mouse and touch events
        var gestureInput = new GenericInput(
            ['mouse', 'touch'],
            {direction : GenericInput.DIRECTION.X}
        );

        gestureInput.subscribe(this.background);

        // Drags correspond to deltas that get accumulated
        var width;
        var gestureDelta = Stream.lift(function(size, data){
            width = size[0];
            if (!data) return false;

            return data.delta / size[0];
        }, [this.size, gestureInput]);

        // Click on slider hooked up to transition slider value
        var offsetX;
        this.background.on('mousedown', function(event){
            offsetX = event.offsetX;
        });

        this.background.on('mouseup', function(event){
            // Check if the mouse hasn't moved for a "static" click event
            if (event.offsetX !== offsetX) return;
            var ratio = event.offsetX / width;

            // TODO: fix setting bug
            if (this.options.transition){
                this.transition.reset(this.ratio.get());
                this.transition.set(ratio, this.options.transition);
            }
            else this.ratio.set(ratio);
        }.bind(this));

        this.ratio.subscribe(gestureDelta);
        this.ratio.subscribe(this.transitionDelta);

        this.value.on('start', renderValue.bind(this));
        this.value.on('update', renderValue.bind(this));
        this.value.on('end', renderValue.bind(this));
    }

    var prevValue = undefined;
    function renderValue(value){
        if (this.options.precision < 0 || !this.labelContent) return;
        var currValue = value.toFixed(this.options.precision);
        if (currValue !== prevValue)
            this.labelContent.textContent = currValue;
        prevValue = currValue;
    }

    module.exports = Slider;
});