Source: controls/Scrollable.js

var Skinable = require('../core/Skinable'),
    ScrollThumb = require('./ScrollThumb'),
    SliderData = require('../utils/SliderData');

/**
 * A scrollabe control provides a thumb that can be be moved along a fixed track.
 * This is the common ground for ScrollBar and Slider
 *
 * @class Scrollable
 * @extends GOWN.Skinable
 * @memberof GOWN
 * @constructor
 * @param [theme] theme for the radio button {GOWN.Theme}
 */
// TODO: remove setting value (value manipulation is for Slider only)
function Scrollable(theme) {
    /**
     * The scrollable mode
     *
     * @type String
     * @default Scrollable.DESKTOP_MODE
     */
    this.mode = this.mode || Scrollable.DESKTOP_MODE;

    Skinable.call(this, theme);

    /**
     * The scrollable direction
     *
     * @type String
     * @default Scrollable.HORIZONTAL
     */
    this.direction = this.direction || Scrollable.HORIZONTAL;

    /**
     * Invalidate track so that it will be redrawn next time
     *
     * @private
     * @type bool
     * @default true
     */
    this.invalidTrack = true;

    /**
     * Inverse the progress bar
     *
     * @private
     * @type bool
     * @default false
     */
    this._inverse = false;

    /**
     * Point where the mouse hit the scrollable
     *
     * @private
     * @type Number[]
     * @default null
     */
    this._start = null;

    /**
     * The minimum
     *
     * @private
     * @type Number
     * @default 0
     */
    this._minimum = this._minimum || 0;

    /**
     * The maximum
     *
     * @private
     * @type Number
     * @default 100
     */
    this._maximum = this._maximum || 100;

    /**
     * Step size (not implemented yet)
     *
     * @private
     * @type Number
     * @default 1
     */
    this.step = this.step || 1; //TODO: implement me!

    /**
     * Pagination jump (not implemented yet)
     *
     * @private
     * @type Number
     * @default 10
     */
    this.page = this.page || 10; //TODO: implement me!

    /**
     * Value
     *
     * @private
     * @type Number
     * @default 0
     */
    this._value = this.minimum;

    /**
     * Number of pixels you scroll at a time (if the event delta is 1 / -1)
     *
     * @type Number
     * @default 10
     */
    this.scrolldelta = 10;

    this.on('touchstart', this.handleDown, this);
    this.on('mousedown', this.handleDown, this);

    this.on('touchend', this.handleUp, this);
    this.on('touchendoutside', this.handleUp, this);
    this.on('mouseupoutside', this.handleUp, this);
    this.on('mouseup', this.handleUp, this);

    /**
     * Invalidate thumb factory so that it will be redrawn next time
     *
     * @private
     * @type bool
     * @default true
     */
    this.thumbFactoryInvalid = true;
}

Scrollable.prototype = Object.create( Skinable.prototype );
Scrollable.prototype.constructor = Scrollable;
module.exports = Scrollable;

/**
 * In desktop mode mouse wheel support is added (default)
 *
 * @static
 * @final
 * @type String
 */
Scrollable.DESKTOP_MODE = 'desktop';

/**
 * In mobile mode mouse wheel support is disabled
 *
 * @static
 * @final
 * @type String
 */
Scrollable.MOBILE_MODE = 'mobile';

/**
 * Show horizontal scrollbar/slider
 *
 * @static
 * @final
 * @type String
 */
Scrollable.HORIZONTAL = 'horizontal';

/**
 * Show vertical scrollbar/slider
 *
 * @static
 * @final
 * @type String
 */
Scrollable.VERTICAL = 'vertical';

/**
 * Create the thumb
 *
 * @private
 */
Scrollable.prototype.createThumb = function() {
    this._thumbFactory = this._thumbFactory || this.defaultThumbFactory;
    this.thumb = this._thumbFactory();
    this.addChild(this.thumb);
    this.positionThumb(this.value);
};

/**
 * A function that is expected to return a new GOWN.ScrollThumb
 *
 * @returns {ScrollThumb}
 * @private
 */
Scrollable.prototype.defaultThumbFactory = function() {
    return new ScrollThumb(this, this.theme);
};

/**
 * Scroll to a specific position (not implemented yet)
 */
Scrollable.prototype.scrollToPosition = function() {
};

/**
 * Handle mouse down/touch start.
 * Move scroll thumb.
 *
 * @param mouseData mouse data provided by PIXI
 * @protected
 */
Scrollable.prototype.handleDown = function(mouseData) {
    var local = mouseData.data.getLocalPosition(this);
    var center = {
        x: local.x - this.thumb.width / 2,
        y: local.y - this.thumb.height / 2
    };
    if (mouseData.target === this &&
        this.moveThumb(center.x, center.y)) {
        this._start = [local.x, local.y];
        this.thumbMoved(center.x, center.y);
    }
};

/**
 * @private
 */
Scrollable.prototype.decrement = function() {
  this.value -= this._step;
};

/**
 * @private
 */
Scrollable.prototype.increment = function() {
  this.value += this._step;
};

/**
 * Handle mouse up/touch end
 *
 * @protected
 */
Scrollable.prototype.handleUp = function() {
    this._start = null;
};

/**
 * Handle mouse move. Moves the thumb.
 *
 * @param mouseData mouse data provided by PIXI
 * @protected
 */
Scrollable.prototype.handleMove = function(mouseData) {
    if (this._start) {
        var local = mouseData.data.getLocalPosition(this);
        var x = this.thumb.x + local.x - this._start[0];
        var y = this.thumb.y + local.y - this._start[1];
        if (this.moveThumb(x, y)) {
            // do not override localX/localY in start
            // if we do not move the thumb
            this.thumbMoved(x, y);
            this._start[0] = local.x;
            this._start[1] = local.y;
        }
    }
};

/**
 * Handle mouse wheel. Moves thumb on track.
 *
 * @param event mouse wheel event from browser
 * @protected
 */
Scrollable.prototype.handleWheel = function (event) {
    var x = this.thumb.x - event.delta * this.scrolldelta;
    var y = this.thumb.y - event.delta * this.scrolldelta;
    if (this.moveThumb(x, y)) {
        this.thumbMoved(x, y);
    }
};

/**
 * Thumb has new x/y position
 *
 * @param x x-position that has been scrolled to (ignored when vertical) {Number}
 * @param y y-position that has been scrolled to (ignored when horizontal) {Number}
 */
Scrollable.prototype.thumbMoved = function(x, y) {
    var pos = this.direction === Scrollable.HORIZONTAL ? x : y;
    this.value = this.pixelToValue(pos);
};

/**
 * Show the progress skin from the start/end of the scroll track to the current
 * position of the thumb.
 *
 * @private
 */
Scrollable.prototype._updateProgressSkin = function() {
    if (!this.progressSkin) {
        return;
    }
    if(this.direction === Scrollable.HORIZONTAL) {
        var progressPosX = this.thumb.x + this.thumb.width / 2;
        if (this.inverse) {
            this.progressSkin.x = progressPosX;
            this.progressSkin.width = this.width - progressPosX;
            this.progressSkin.height = this.skin.height;
        } else {
            this.progressSkin.x = 0;
            this.progressSkin.width = progressPosX;
            this.progressSkin.height = this.skin.height;
        }
    } else {
        var progressPosY = this.thumb.y + this.thumb.height / 2;
        if (this.inverse) {
            this.progressSkin.y = progressPosY;
            this.progressSkin.height = this.height - progressPosY;
            this.progressSkin.width = this.skin.width;
        } else {
            this.progressSkin.y = 0;
            this.progressSkin.height =progressPosY;
            this.progressSkin.width = this.skin.width;
        }
    }
};

/**
 * Returns the max. width in pixel
 * (normally this.width - thumb width)
 *
 * @returns {Number}
 */
Scrollable.prototype.maxWidth = function() {
    return this.width - this.thumb.width;
};

/**
 * Returns the max. height in pixel
 * (normally this.height - thumb height)
 *
 * @returns {Number}
 */
Scrollable.prototype.maxHeight = function() {
    return this.height - this.thumb.height;
};

/**
 * Move the thumb on the scroll bar within its bounds
 *
 * @param x New x position of the thumb {Number}
 * @param y New y position of the thumb {Number}
 * @returns {boolean} Returns true if the position of the thumb has been
 * moved
 */
Scrollable.prototype.moveThumb = function(x, y) {
    if (this.thumb.move(x, y)) {
        this._updateProgressSkin();
        return true;
    }
    return false;
};

/**
 * Show scroll track
 *
 * @param skin The track skin {PIXI.DisplayObject}
 * @private
 */
Scrollable.prototype.showTrack = function(skin) {
    if (this.skin !== skin) {
        if(this.skin) {
            this.removeChild(this.skin);
        }

        this.addChildAt(skin, 0);
        this.skin = skin;
        if (this.progressSkin) {
            this._updateProgressSkin();
        }
    }
};

/**
 * Show progress on track (from the start/end of the track to the
 * current position of the thumb)
 *
 * @param skin The progress skin {PIXI.DisplayObject}
 * @private
 */
Scrollable.prototype.showProgress = function(skin) {
    if (this.progressSkin !== skin) {
        if(this.progressSkin) {
            this.removeChild(this.progressSkin);
        }
        skin.width = skin.height = 0;
        this.addChildAt(skin, 0);
        this.progressSkin = skin;
        if (this.skin) {
            this._updateProgressSkin();
        }
    }
};

/**
 * Update before draw call. Redraw track and progressbar and create thumb.
 *
 * @protected
 */
Scrollable.prototype.redraw = function() {
    if (this.thumbFactoryInvalid) {
        this.createThumb();
        this.thumbFactoryInvalid = false;
    }
    if (this.invalidTrack) {
        this.fromSkin(this.direction+'_progress', this.showProgress);
        this.fromSkin(this.direction+'_track', this.showTrack);
        if (this.skin) {
            if (this.direction === Scrollable.HORIZONTAL) {
                this.skin.width = this.width;
            } else {
                this.skin.height = this.height;
            }
            this.invalidTrack = false;
        }
    }
};

/**
 * Calculate value of slider based on the current pixel position of the thumb
 *
 * @param position current pixel position of the thumb {Number}
 * @returns {Number} Value between minimum and maximum
 */
Scrollable.prototype.pixelToValue = function(position) {
    var max = 0;
    if (this.direction === Scrollable.HORIZONTAL) {
        max = this.maxWidth();
    } else {
        max = this.maxHeight();
    }
    if (this._inverse) {
        position = max - position;
    }
    return position / max * (this.maximum - this.minimum) + this.minimum;
};

/**
 * Calculate current pixel position of thumb based on given value
 *
 * @param value The value of the thumb position {Number}
 * @returns {Number} Position of the scroll thumb in pixel
 */
Scrollable.prototype.valueToPixel = function(value) {
    var max = 0;
    if (this.direction === Scrollable.HORIZONTAL) {
        max = this.maxWidth();
    } else {
        max = this.maxHeight();
    }
    var position = (value - this.minimum) / (this.maximum - this.minimum) * max;
    if (this._inverse) {
        position = max - position;
    }
    return position;
};

/**
 * Position the thumb to a given value
 *
 * @param value The value to which the thumb gets moved {Number}
 */
Scrollable.prototype.positionThumb = function(value) {
    if (this.thumb) {
        var pos = this.valueToPixel(value);
        if (this.direction === Scrollable.HORIZONTAL) {
            this.moveThumb(pos, 0);
        } else {
            this.moveThumb(0, pos);
        }
    }
};

/**
 * The width of the Scrollable, setting this will redraw the track and thumb.
 *
 * @name GOWN.Scrollable#width
 * @type Number
 */
Object.defineProperty(Scrollable.prototype, 'width', {
    get: function() {
        return this._width;
    },
    set: function(width) {
        this._width = width;
        this.invalidTrack = true;
        if (this.thumb) {
            this.thumb.invalidTrack = true;
        }
    }
});

/**
 * Inverse the progress bar
 *
 * @name GOWN.Scrollable#inverse
 * @type Boolean
 */
Object.defineProperty(Scrollable.prototype, 'inverse', {
    get: function() {
        return this._inverse;
    },
    set: function(inverse) {
        if (inverse !== this._inverse) {
            this._inverse = inverse;

            if (this.direction === Scrollable.HORIZONTAL) {
                this.moveThumb(this.width - this.thumb.x, 0);
            } else {
                this.moveThumb(0, this.height - this.thumb.y);
            }

            this.invalidTrack = true;
            if (this.thumb) {
                this.thumb.invalidTrack = true;
            }
        }
    }
});

/**
 * The height of the Scrollable, setting this will redraw the track and thumb.
 *
 * @name GOWN.Scrollable#height
 * @type Number
 */
Object.defineProperty(Scrollable.prototype, 'height', {
    get: function() {
        return this._height;
    },
    set: function(height) {
        this._height = height;
        this.invalidTrack = true;
        if (this.thumb) {
            this.thumb.invalidTrack = true;
        }
    }
});

/**
 * Set value (between minimum and maximum)
 *
 * @name GOWN.Scrollable#value
 * @type Number
 * @default 0
 */
Object.defineProperty(Scrollable.prototype, 'value', {
    get: function() {
        return this._value;
    },
    set: function(value) {
        if (isNaN(value)) {
            return;
        }
        value = Math.min(value, this.maximum);
        value = Math.max(value, this.minimum);
        if (this._value === value) {
            return;
        }

        // inform system that value has been changed
        var sliderData = new SliderData();
        sliderData.value = value;
        sliderData.target = this;
        if (this.change) {
            this.change(sliderData);
        }
        this.emit('change', sliderData, this);

        // move thumb
        this.positionThumb(value);

        this._value = value;
    }
});

/**
 * Set minimum and update value if necessary
 *
 * @name GOWN.Scrollable#minimum
 * @type Number
 * @default 0
 */
Object.defineProperty(Scrollable.prototype, 'minimum', {
    get: function() {
        return this._minimum;
    },
    set: function(minimum) {
        if(!isNaN(minimum) && this.minimum !== minimum && minimum < this.maximum) {
            this._minimum = minimum;
        }
        if (this._value < this.minimum) {
            this.value = this._value;
        }
    }
});

/**
 * Set maximum and update value if necessary
 *
 * @name GOWN.Scrollable#maximum
 * @type Number
 * @default 100
 */
Object.defineProperty(Scrollable.prototype, 'maximum', {
    get: function() {
        return this._maximum;
    },
    set: function(maximum) {
        if(!isNaN(maximum) && this.maximum !== maximum && maximum > this.minimum) {
            this._maximum = maximum;
        }
        if (this._value > this.maximum) {
            this.value = maximum;
        }
    }
});