andreruffert / rangeslider.js

🎚 HTML5 input range slider element jQuery polyfill
https://rangeslider.js.org
MIT License
2.17k stars 401 forks source link

Vertical range? #11

Closed ghost closed 9 years ago

ghost commented 10 years ago

This looks like a great polyfill/plugin but I need to use it for vertical range's too (volume control in a video player. Is this something you're considering?

andreruffert commented 10 years ago

Yes why not could be a good enhancement but I'm very busy atm. I'll check it out if I find time...

ghost commented 10 years ago

No worries. I know the feeling! No rush, I'm just working on the player in my spare time too.

abhishiv commented 10 years ago

Just to say that would be a great feature.

andreruffert commented 10 years ago

@abhishiv I think so too. I'll definitely implement it if I find time.

danimalweb commented 9 years ago

Another this would be awesome comment.

iamdriz commented 9 years ago

For those that need to implement it now here is a modified version that ONLY supports vertical:

/*! rangeslider.js - v1.2.1 | (c) 2015 @andreruffert | MIT license | https://github.com/andreruffert/rangeslider.js */
(function(factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    }
    else if (typeof exports === 'object') {
        // CommonJS
        factory(require('jquery'));
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function($) {
    'use strict';

    /**
     * Range feature detection
     * @return {Boolean}
     */
    function supportsRange() {
        var input = document.createElement('input');
        input.setAttribute('type', 'range');
        return input.type !== 'text';
    }

    var pluginName = 'rangeslider',
        pluginIdentifier = 0,
        inputrange = supportsRange(),
        defaults = {
            polyfill: true,
            rangeClass: 'rangeslider',
            disabledClass: 'rangeslider--disabled',
            fillClass: 'rangeslider__fill',
            handleClass: 'rangeslider__handle',
            startEvent: ['mousedown', 'touchstart', 'pointerdown'],
            moveEvent: ['mousemove', 'touchmove', 'pointermove'],
            endEvent: ['mouseup', 'touchend', 'pointerup']
        };

    /**
     * Delays a function for the given number of milliseconds, and then calls
     * it with the arguments supplied.
     *
     * @param  {Function} fn   [description]
     * @param  {Number}   wait [description]
     * @return {Function}
     */
    function delay(fn, wait) {
        var args = Array.prototype.slice.call(arguments, 2);
        return setTimeout(function(){ return fn.apply(null, args); }, wait);
    }

    /**
     * Returns a debounced function that will make sure the given
     * function is not triggered too much.
     *
     * @param  {Function} fn Function to debounce.
     * @param  {Number}   debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
     * @return {Function}
     */
    function debounce(fn, debounceDuration) {
        debounceDuration = debounceDuration || 100;
        return function() {
            if (!fn.debouncing) {
                var args = Array.prototype.slice.apply(arguments);
                fn.lastReturnVal = fn.apply(window, args);
                fn.debouncing = true;
            }
            clearTimeout(fn.debounceTimeout);
            fn.debounceTimeout = setTimeout(function(){
                fn.debouncing = false;
            }, debounceDuration);
            return fn.lastReturnVal;
        };
    }

    /**
     * Check if a `element` is visible in the DOM
     *
     * @param  {Element}  element
     * @return {Boolean}
     */
    function isHidden(element) {
        return (
            element && (
                element.offsetWidth === 0 ||
                element.offsetHeight === 0 ||
                // Also Consider native `<details>` elements.
                element.open === false
            )
        );
    }

    /**
     * Get hidden parentNodes of an `element`
     *
     * @param  {Element} element
     * @return {[type]}
     */
    function getHiddenParentNodes(element) {
        var parents = [],
            node    = element.parentNode;

        while (isHidden(node)) {
            parents.push(node);
            node = node.parentNode;
        }
        return parents;
    }

    /**
     * Returns dimensions for an element even if it is not visible in the DOM.
     *
     * @param  {Element} element
     * @param  {String}  key     (e.g. offsetWidth …)
     * @return {Number}
     */
    function getDimension(element, key) {
        var hiddenParentNodes       = getHiddenParentNodes(element),
            hiddenParentNodesLength = hiddenParentNodes.length,
            inlineStyle             = [],
            dimension               = element[key];

        // Used for native `<details>` elements
        function toggleOpenProperty(element) {
            if (typeof element.open !== 'undefined') {
                element.open = (element.open) ? false : true;
            }
        }

        if (hiddenParentNodesLength) {
            for (var i = 0; i < hiddenParentNodesLength; i++) {

                // Cache style attribute to restore it later.
                inlineStyle[i] = hiddenParentNodes[i].style.cssText;

                // visually hide
                hiddenParentNodes[i].style.display = 'block';
                hiddenParentNodes[i].style.height = '0';
                hiddenParentNodes[i].style.overflow = 'hidden';
                hiddenParentNodes[i].style.visibility = 'hidden';
                toggleOpenProperty(hiddenParentNodes[i]);
            }

            // Update dimension
            dimension = element[key];

            for (var j = 0; j < hiddenParentNodesLength; j++) {

                // Restore the style attribute
                hiddenParentNodes[j].style.cssText = inlineStyle[j];
                toggleOpenProperty(hiddenParentNodes[j]);
            }
        }
        return dimension;
    }

    /**
     * Plugin
     * @param {String} element
     * @param {Object} options
     */
    function Plugin(element, options) {
        this.$window    = $(window);
        this.$document  = $(document);
        this.$element   = $(element);
        this.options    = $.extend( {}, defaults, options );
        this.polyfill   = this.options.polyfill;
        this.onInit     = this.options.onInit;
        this.onSlide    = this.options.onSlide;
        this.onSlideEnd = this.options.onSlideEnd;

        // Plugin should only be used as a polyfill
        if (this.polyfill) {
            // Input range support?
            if (inputrange) { return false; }
        }

        this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
        this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.moveEvent  = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.endEvent   = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.toFixed    = (this.step + '').replace('.', '').length - 1;
        this.$fill      = $('<div class="' + this.options.fillClass + '" />');
        this.$handle    = $('<div class="' + this.options.handleClass + '" />');
        this.$range     = $('<div class="' + this.options.rangeClass + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);

        // visually hide the input
        this.$element.css({
            'position': 'absolute',
            'width': '1px',
            'height': '1px',
            'overflow': 'hidden',
            'opacity': '0'
        });

        // Store context
        this.handleDown = $.proxy(this.handleDown, this);
        this.handleMove = $.proxy(this.handleMove, this);
        this.handleEnd  = $.proxy(this.handleEnd, this);

        this.init();

        // Attach Events
        var _this = this;
        this.$window.on('resize.' + this.identifier, debounce(function() {
            // Simulate resizeEnd event.
            delay(function() { _this.update(); }, 300);
        }, 20));

        this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDown);

        // Listen to programmatic value changes
        this.$element.on('change.' + this.identifier, function(e, data) {
            if (data && data.origin === _this.identifier) {
                return;
            }

            var value = e.target.value,
                pos = _this.getPositionFromValue(value);
            _this.setPosition(pos);
        });
    }

    Plugin.prototype.init = function() {
        this.update(true);

        // Set initial value just in case it is not set already.
        // Prevents trouble if we call `update(true)`
        this.$element[0].value = this.value;

        if (this.onInit && typeof this.onInit === 'function') {
            this.onInit();
        }
    };

    Plugin.prototype.update = function(updateAttributes) {
        updateAttributes = updateAttributes || false;

        if (updateAttributes) {
            this.min    = parseFloat(this.$element[0].getAttribute('min') || 0);
            this.max    = parseFloat(this.$element[0].getAttribute('max') || 100);
            this.value  = parseFloat(this.$element[0].value || this.min + (this.max-this.min)/2);
            this.step   = parseFloat(this.$element[0].getAttribute('step') || 1);
        }

        this.handleHeight    = getDimension(this.$handle[0], 'offsetHeight');
        this.rangeHeight     = getDimension(this.$range[0], 'offsetHeight');
        this.maxHandleY     = this.rangeHeight - this.handleHeight;
        this.grabY          = this.handleHeight / 2;
        this.position       = this.getPositionFromValue(this.value);

        // Consider disabled state
        if (this.$element[0].disabled) {
            this.$range.addClass(this.options.disabledClass);
        } else {
            this.$range.removeClass(this.options.disabledClass);
        }

        this.setPosition(this.position);
    };

    Plugin.prototype.handleDown = function(e) {
        e.preventDefault();
        this.$document.on(this.moveEvent, this.handleMove);
        this.$document.on(this.endEvent, this.handleEnd);

        // If we click on the handle don't set the new position
        if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
            return;
        }

        var posY    = this.getRelativePosition(e),
            rangeY  = this.$range[0].getBoundingClientRect().top,
            handleY = this.getPositionFromNode(this.$handle[0]) - rangeY;

        this.setPosition(posY - this.grabY);

        if (posY >= handleY && posY < handleY + this.handleHeight) {
            this.grabY = posY - handleY;
        }
    };

    Plugin.prototype.handleMove = function(e) {
        e.preventDefault();
        var posY = this.getRelativePosition(e);
        this.setPosition(posY - this.grabY);
    };

    Plugin.prototype.handleEnd = function(e) {
        e.preventDefault();
        this.$document.off(this.moveEvent, this.handleMove);
        this.$document.off(this.endEvent, this.handleEnd);

        // Ok we're done fire the change event
        this.$element.trigger('change', { origin: this.identifier });

        if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
            this.onSlideEnd(this.position, this.value);
        }
    };

    Plugin.prototype.cap = function(pos, min, max) {
        if (pos < min) { return min; }
        if (pos > max) { return max; }
        return pos;
    };

    Plugin.prototype.setPosition = function(pos) {
        var value, top;

        // Snapping steps
        value = this.getValueFromPosition(this.cap(pos, 0, this.maxHandleY));
        top = this.getPositionFromValue(value);

        // Update ui
        this.$fill[0].style.height = (top + this.grabY) + 'px';
        this.$handle[0].style.top = top + 'px';
        this.setValue(value);

        // Update globals
        this.position = top;
        this.value = value;

        if (this.onSlide && typeof this.onSlide === 'function') {
            this.onSlide(top, value);
        }
    };

    // Returns element position relative to the parent
    Plugin.prototype.getPositionFromNode = function(node) {
        var i = 0;
        while (node !== null) {
            i += node.offsetLeft;
            node = node.offsetParent;
        }
        return i;
    };

    Plugin.prototype.getRelativePosition = function(e) {
        // Get the offset left relative to the viewport
        var rangeY  = this.$range[0].getBoundingClientRect().top,
            pageY   = 0;

        if (typeof e.pageY !== 'undefined') {
            pageY = e.pageY;
        }
        else if (typeof e.originalEvent.clientY !== 'undefined') {
            pageY = e.originalEvent.clientY;
        }
        else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0].clientY !== 'undefined') {
            pageY = e.originalEvent.touches[0].clientX;
        }
        else if(e.currentPoint && typeof e.currentPoint.y !== 'undefined') {
            pageY = e.currentPoint.y;
        }

        return pageY - rangeY;
    };

    Plugin.prototype.getPositionFromValue = function(value) {
        var percentage, pos;
        percentage = (value - this.min)/(this.max - this.min);
        pos = percentage * this.maxHandleY;
        return pos;
    };

    Plugin.prototype.getValueFromPosition = function(pos) {
        var percentage, value;
        percentage = ((pos) / (this.maxHandleY || 1));
        value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
        return Number((value).toFixed(this.toFixed));
    };

    Plugin.prototype.setValue = function(value) {
        if (value === this.value) {
            return;
        }

        // Set the new value and fire the `input` event
        this.$element
            .val(value)
            .trigger('input', { origin: this.identifier });
    };

    Plugin.prototype.destroy = function() {
        this.$document.off('.' + this.identifier);
        this.$window.off('.' + this.identifier);

        this.$element
            .off('.' + this.identifier)
            .removeAttr('style')
            .removeData('plugin_' + pluginName);

        // Remove the generated markup
        if (this.$range && this.$range.length) {
            this.$range[0].parentNode.removeChild(this.$range[0]);
        }
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[pluginName] = function(options) {
        var args = Array.prototype.slice.call(arguments, 1);

        return this.each(function() {
            var $this = $(this),
                data  = $this.data('plugin_' + pluginName);

            // Create a new instance.
            if (!data) {
                $this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
            }

            // Make it possible to access methods from public.
            // e.g `$element.rangeslider('method');`
            if (typeof options === 'string') {
                data[options].apply(data, args);
            }
        });
    };

}));
iamdriz commented 9 years ago

And in reverse for those that need it for a volume control:

(function(factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    }
    else if (typeof exports === 'object') {
        // CommonJS
        factory(require('jquery'));
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function($) {
    'use strict';

    /**
     * Range feature detection
     * @return {Boolean}
     */
    function supportsRange() {
        var input = document.createElement('input');
        input.setAttribute('type', 'range');
        return input.type !== 'text';
    }

    var pluginName = 'rangeslider',
        pluginIdentifier = 0,
        inputrange = supportsRange(),
        defaults = {
            polyfill: true,
            rangeClass: 'rangeslider',
            disabledClass: 'rangeslider--disabled',
            fillClass: 'rangeslider__fill',
            handleClass: 'rangeslider__handle',
            startEvent: ['mousedown', 'touchstart', 'pointerdown'],
            moveEvent: ['mousemove', 'touchmove', 'pointermove'],
            endEvent: ['mouseup', 'touchend', 'pointerup']
        };

    /**
     * Delays a function for the given number of milliseconds, and then calls
     * it with the arguments supplied.
     *
     * @param  {Function} fn   [description]
     * @param  {Number}   wait [description]
     * @return {Function}
     */
    function delay(fn, wait) {
        var args = Array.prototype.slice.call(arguments, 2);
        return setTimeout(function(){ return fn.apply(null, args); }, wait);
    }

    /**
     * Returns a debounced function that will make sure the given
     * function is not triggered too much.
     *
     * @param  {Function} fn Function to debounce.
     * @param  {Number}   debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
     * @return {Function}
     */
    function debounce(fn, debounceDuration) {
        debounceDuration = debounceDuration || 100;
        return function() {
            if (!fn.debouncing) {
                var args = Array.prototype.slice.apply(arguments);
                fn.lastReturnVal = fn.apply(window, args);
                fn.debouncing = true;
            }
            clearTimeout(fn.debounceTimeout);
            fn.debounceTimeout = setTimeout(function(){
                fn.debouncing = false;
            }, debounceDuration);
            return fn.lastReturnVal;
        };
    }

    /**
     * Check if a `element` is visible in the DOM
     *
     * @param  {Element}  element
     * @return {Boolean}
     */
    function isHidden(element) {
        return (
            element && (
                element.offsetWidth === 0 ||
                element.offsetHeight === 0 ||
                // Also Consider native `<details>` elements.
                element.open === false
            )
        );
    }

    /**
     * Get hidden parentNodes of an `element`
     *
     * @param  {Element} element
     * @return {[type]}
     */
    function getHiddenParentNodes(element) {
        var parents = [],
            node    = element.parentNode;

        while (isHidden(node)) {
            parents.push(node);
            node = node.parentNode;
        }
        return parents;
    }

    /**
     * Returns dimensions for an element even if it is not visible in the DOM.
     *
     * @param  {Element} element
     * @param  {String}  key     (e.g. offsetWidth …)
     * @return {Number}
     */
    function getDimension(element, key) {
        var hiddenParentNodes       = getHiddenParentNodes(element),
            hiddenParentNodesLength = hiddenParentNodes.length,
            inlineStyle             = [],
            dimension               = element[key];

        // Used for native `<details>` elements
        function toggleOpenProperty(element) {
            if (typeof element.open !== 'undefined') {
                element.open = (element.open) ? false : true;
            }
        }

        if (hiddenParentNodesLength) {
            for (var i = 0; i < hiddenParentNodesLength; i++) {

                // Cache style attribute to restore it later.
                inlineStyle[i] = hiddenParentNodes[i].style.cssText;

                // visually hide
                hiddenParentNodes[i].style.display = 'block';
                hiddenParentNodes[i].style.height = '0';
                hiddenParentNodes[i].style.overflow = 'hidden';
                hiddenParentNodes[i].style.visibility = 'hidden';
                toggleOpenProperty(hiddenParentNodes[i]);
            }

            // Update dimension
            dimension = element[key];

            for (var j = 0; j < hiddenParentNodesLength; j++) {

                // Restore the style attribute
                hiddenParentNodes[j].style.cssText = inlineStyle[j];
                toggleOpenProperty(hiddenParentNodes[j]);
            }
        }
        return dimension;
    }

    /**
     * Plugin
     * @param {String} element
     * @param {Object} options
     */
    function Plugin(element, options) {
        this.$window    = $(window);
        this.$document  = $(document);
        this.$element   = $(element);
        this.options    = $.extend( {}, defaults, options );
        this.polyfill   = this.options.polyfill;
        this.onInit     = this.options.onInit;
        this.onSlide    = this.options.onSlide;
        this.onSlideEnd = this.options.onSlideEnd;

        // Plugin should only be used as a polyfill
        if (this.polyfill) {
            // Input range support?
            if (inputrange) { return false; }
        }

        this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
        this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.moveEvent  = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.endEvent   = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.toFixed    = (this.step + '').replace('.', '').length - 1;
        this.$fill      = $('<div class="' + this.options.fillClass + '" />');
        this.$handle    = $('<div class="' + this.options.handleClass + '" />');
        this.$range     = $('<div class="' + this.options.rangeClass + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);

        // visually hide the input
        this.$element.css({
            'position': 'absolute',
            'width': '1px',
            'height': '1px',
            'overflow': 'hidden',
            'opacity': '0'
        });

        // Store context
        this.handleDown = $.proxy(this.handleDown, this);
        this.handleMove = $.proxy(this.handleMove, this);
        this.handleEnd  = $.proxy(this.handleEnd, this);

        this.init();

        // Attach Events
        var _this = this;
        this.$window.on('resize.' + this.identifier, debounce(function() {
            // Simulate resizeEnd event.
            delay(function() { _this.update(); }, 300);
        }, 20));

        this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDown);

        // Listen to programmatic value changes
        this.$element.on('change.' + this.identifier, function(e, data) {
            if (data && data.origin === _this.identifier) {
                return;
            }

            var value = e.target.value,
                pos = _this.getPositionFromValue(value);
            _this.setPosition(pos);
        });
    }

    Plugin.prototype.init = function() {
        this.update(true);

        // Set initial value just in case it is not set already.
        // Prevents trouble if we call `update(true)`
        this.$element[0].value = this.value;

        if (this.onInit && typeof this.onInit === 'function') {
            this.onInit();
        }
    };

    Plugin.prototype.update = function(updateAttributes) {
        updateAttributes = updateAttributes || false;

        if (updateAttributes) {
            this.min    = parseFloat(this.$element[0].getAttribute('min') || 0);
            this.max    = parseFloat(this.$element[0].getAttribute('max') || 100);
            this.value  = parseFloat(this.$element[0].value || this.min + (this.max-this.min)/2);
            this.step   = parseFloat(this.$element[0].getAttribute('step') || 1);
        }

        this.handleHeight    = getDimension(this.$handle[0], 'offsetHeight');
        this.rangeHeight     = getDimension(this.$range[0], 'offsetHeight');
        this.maxHandleY     = this.rangeHeight - this.handleHeight;
        this.grabY          = this.handleHeight / 2;
        this.position       = this.getPositionFromValue(this.value);

        // Consider disabled state
        if (this.$element[0].disabled) {
            this.$range.addClass(this.options.disabledClass);
        } else {
            this.$range.removeClass(this.options.disabledClass);
        }

        this.setPosition(this.position);
    };

    Plugin.prototype.handleDown = function(e) {
        e.preventDefault();
        this.$document.on(this.moveEvent, this.handleMove);
        this.$document.on(this.endEvent, this.handleEnd);

        // If we click on the handle don't set the new position
        if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
            return;
        }

        var posY    = this.getRelativePosition(e),
            rangeY  = this.$range[0].getBoundingClientRect().top,
            handleY = this.getPositionFromNode(this.$handle[0]) - rangeY;

        //this.setPosition(posY - this.grabY);
        this.setPosition(this.maxHandleY - (posY - this.grabY));

        if (posY >= handleY && posY < handleY + this.handleHeight) {
            this.grabY = posY - handleY;
        }
    };

    Plugin.prototype.handleMove = function(e) {
        e.preventDefault();
        var posY = this.getRelativePosition(e);
        //this.setPosition(posY - this.grabY);
        this.setPosition(this.maxHandleY - (posY - this.grabY));
    };

    Plugin.prototype.handleEnd = function(e) {
        e.preventDefault();
        this.$document.off(this.moveEvent, this.handleMove);
        this.$document.off(this.endEvent, this.handleEnd);

        // Ok we're done fire the change event
        this.$element.trigger('change', { origin: this.identifier });

        if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
            this.onSlideEnd(this.position, this.value);
        }
    };

    Plugin.prototype.cap = function(pos, min, max) {
        if (pos < min) { return min; }
        if (pos > max) { return max; }
        return pos;
    };

    Plugin.prototype.setPosition = function(pos) {
        var value, bottom;

        // Snapping steps
        value = this.getValueFromPosition(this.cap(pos, 0, this.maxHandleY));
        bottom = this.getPositionFromValue(value);

        // Update ui
        this.$fill[0].style.height = (bottom + this.grabY) + 'px';
        this.$handle[0].style.bottom = bottom + 'px';
        this.setValue(value);

        // Update globals
        this.position = bottom;
        this.value = value;

        if (this.onSlide && typeof this.onSlide === 'function') {
            this.onSlide(bottom, value);
        }
    };

    // Returns element position relative to the parent
    Plugin.prototype.getPositionFromNode = function(node) {
        var i = 0;
        while (node !== null) {
            i += node.offsetLeft;
            node = node.offsetParent;
        }
        return i;
    };

    Plugin.prototype.getRelativePosition = function(e) {
        // Get the offset left relative to the viewport
        var rangeY  = this.$range[0].getBoundingClientRect().top,
            pageY   = 0;

        if (typeof e.pageY !== 'undefined') {
            pageY = e.pageY;
        }
        else if (typeof e.originalEvent.clientY !== 'undefined') {
            pageY = e.originalEvent.clientY;
        }
        else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0].clientY !== 'undefined') {
            pageY = e.originalEvent.touches[0].clientY;
        }
        else if(e.currentPoint && typeof e.currentPoint.y !== 'undefined') {
            pageY = e.currentPoint.y;
        }
        return pageY - rangeY;
    };

    Plugin.prototype.getPositionFromValue = function(value) {
        var percentage, pos;
        percentage = (value - this.min)/(this.max - this.min);
        pos = percentage * this.maxHandleY;
        return pos;
    };

    Plugin.prototype.getValueFromPosition = function(pos) {
        var percentage, value;
        percentage = ((pos) / (this.maxHandleY || 1));
        value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
        return Number((value).toFixed(this.toFixed));
    };

    Plugin.prototype.setValue = function(value) {

        if (value === this.value) {
            return;
        }

        // Set the new value and fire the `input` event
        this.$element
            .val(value)
            .trigger('input', { origin: this.identifier });
    };

    Plugin.prototype.destroy = function() {
        this.$document.off('.' + this.identifier);
        this.$window.off('.' + this.identifier);

        this.$element
            .off('.' + this.identifier)
            .removeAttr('style')
            .removeData('plugin_' + pluginName);

        // Remove the generated markup
        if (this.$range && this.$range.length) {
            this.$range[0].parentNode.removeChild(this.$range[0]);
        }
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[pluginName] = function(options) {
        var args = Array.prototype.slice.call(arguments, 1);

        return this.each(function() {
            var $this = $(this),
                data  = $this.data('plugin_' + pluginName);

            // Create a new instance.
            if (!data) {
                $this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
            }

            // Make it possible to access methods from public.
            // e.g `$element.rangeslider('method');`
            if (typeof options === 'string') {
                data[options].apply(data, args);
            }
        });
    };

}));
chriscamplin commented 9 years ago

I've modified this to include a horizontal / vertical option:

chriscamplin commented 9 years ago
/*! rangeslider.js - v1.2.2 | (c) 2015 @andreruffert | MIT license | https://github.com/andreruffert/rangeslider.js */
(function(factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    }
    else if (typeof exports === 'object') {
        // CommonJS
        factory(require('jquery'));
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function($) {
    'use strict';

    // Polyfill Number.isNaN(value)
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
    Number.isNaN = Number.isNaN || function(value) {
        return typeof value === 'number' && value !== value;
    };

    /**
     * Range feature detection
     * @return {Boolean}
     */
    function supportsRange() {
        var input = document.createElement('input');
        input.setAttribute('type', 'range');
        return input.type !== 'text';
    }

    var pluginName = 'rangeslider',
        pluginIdentifier = 0,
        inputrange = supportsRange(),
        defaults = {
            polyfill: true,
            sliderOrientationHoriz: true,
            rangeClass: 'rangeslider',
            disabledClass: 'rangeslider--disabled',
            fillClass: 'rangeslider__fill',
            handleClass: 'rangeslider__handle',
            startEvent: ['mousedown', 'touchstart', 'pointerdown'],
            moveEvent: ['mousemove', 'touchmove', 'pointermove'],
            endEvent: ['mouseup', 'touchend', 'pointerup']
        };
    /**
     * Delays a function for the given number of milliseconds, and then calls
     * it with the arguments supplied.
     *
     * @param  {Function} fn   [description]
     * @param  {Number}   wait [description]
     * @return {Function}
     */
    function delay(fn, wait) {
        var args = Array.prototype.slice.call(arguments, 2);
        return setTimeout(function(){ return fn.apply(null, args); }, wait);
    }

    /**
     * Returns a debounced function that will make sure the given
     * function is not triggered too much.
     *
     * @param  {Function} fn Function to debounce.
     * @param  {Number}   debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
     * @return {Function}
     */
    function debounce(fn, debounceDuration) {
        debounceDuration = debounceDuration || 100;
        return function() {
            if (!fn.debouncing) {
                var args = Array.prototype.slice.apply(arguments);
                fn.lastReturnVal = fn.apply(window, args);
                fn.debouncing = true;
            }
            clearTimeout(fn.debounceTimeout);
            fn.debounceTimeout = setTimeout(function(){
                fn.debouncing = false;
            }, debounceDuration);
            return fn.lastReturnVal;
        };
    }

    /**
     * Check if a `element` is visible in the DOM
     *
     * @param  {Element}  element
     * @return {Boolean}
     */
    function isHidden(element) {
        return (
            element && (
                element.offsetWidth === 0 ||
                element.offsetHeight === 0 ||
                // Also Consider native `<details>` elements.
                element.open === false
            )
        );
    }

    /**
     * Get hidden parentNodes of an `element`
     *
     * @param  {Element} element
     * @return {[type]}
     */
    function getHiddenParentNodes(element) {
        var parents = [],
            node    = element.parentNode;

        while (isHidden(node)) {
            parents.push(node);
            node = node.parentNode;
        }
        return parents;
    }

    /**
     * Returns dimensions for an element even if it is not visible in the DOM.
     *
     * @param  {Element} element
     * @param  {String}  key     (e.g. offsetWidth …)
     * @return {Number}
     */
    function getDimension(element, key) {
        var hiddenParentNodes       = getHiddenParentNodes(element),
            hiddenParentNodesLength = hiddenParentNodes.length,
            inlineStyle             = [],
            dimension               = element[key];

        // Used for native `<details>` elements
        function toggleOpenProperty(element) {
            if (typeof element.open !== 'undefined') {
                element.open = (element.open) ? false : true;
            }
        }

        if (hiddenParentNodesLength) {
            for (var i = 0; i < hiddenParentNodesLength; i++) {

                // Cache style attribute to restore it later.
                inlineStyle[i] = hiddenParentNodes[i].style.cssText;

                // visually hide
                hiddenParentNodes[i].style.display = 'block';
                hiddenParentNodes[i].style.height = '0';
                hiddenParentNodes[i].style.overflow = 'hidden';
                hiddenParentNodes[i].style.visibility = 'hidden';
                toggleOpenProperty(hiddenParentNodes[i]);
            }

            // Update dimension
            dimension = element[key];

            for (var j = 0; j < hiddenParentNodesLength; j++) {

                // Restore the style attribute
                hiddenParentNodes[j].style.cssText = inlineStyle[j];
                toggleOpenProperty(hiddenParentNodes[j]);
            }
        }
        return dimension;
    }

    /**
     * Returns the parsed float or the default if it failed.
     *
     * @param  {String}  str
     * @param  {Number}  defaultValue
     * @return {Number}
     */
    function tryParseFloat(str, defaultValue) {
        var value = parseFloat(str);
        return Number.isNaN(value) ? defaultValue : value;
    }

    /**
     * Plugin
     * @param {String} element
     * @param {Object} options
     */
    function Plugin(element, options) {
        this.$window    = $(window);
        this.$document  = $(document);
        this.$element   = $(element);
        this.options    = $.extend( {}, defaults, options );
        this.polyfill   = this.options.polyfill;
        this.sliderOrientationHoriz = this.options.sliderOrientationHoriz;
        this.onInit     = this.options.onInit;
        this.onSlide    = this.options.onSlide;
        this.onSlideEnd = this.options.onSlideEnd;

        // Plugin should only be used as a polyfill
        if (this.polyfill) {
            // Input range support?
            if (inputrange) { return false; }
        }

        this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
        this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.moveEvent  = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.endEvent   = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
        this.toFixed    = (this.step + '').replace('.', '').length - 1;
        this.$fill      = $('<div class="' + this.options.fillClass + '" />');
        this.$handle    = $('<div class="' + this.options.handleClass + '" />');
        this.$range     = $('<div class="' + this.options.rangeClass + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);

        // visually hide the input
        this.$element.css({
            'position': 'absolute',
            'width': '1px',
            'height': '1px',
            'overflow': 'hidden',
            'opacity': '0'
        });
        //console.log(this.sliderOrientationHoriz);

        if (this.sliderOrientationHoriz) {
            // Store context
            this.handleDownX = $.proxy(this.handleDownX, this);
            this.handleMoveX = $.proxy(this.handleMoveX, this);
            this.handleEndX  = $.proxy(this.handleEndX, this);
            this.initX();

            // Attach Events
            var _this = this;
            this.$window.on('resize.' + this.identifier, debounce(function() {
                // Simulate resizeEnd event.
                delay(function() { _this.updateX(); }, 300);
            }, 20));

            this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDownX);

            // Listen to programmatic value changes
            this.$element.on('change.' + this.identifier, function(e, data) {
                if (data && data.origin === _this.identifier) {
                    return;
                }

                var value = e.target.value,
                    pos = _this.getPositionFromValueX(value);
                _this.setPositionX(pos);
        });

        }  else {
            this.handleDownY = $.proxy(this.handleDownY, this);
            this.handleMoveY = $.proxy(this.handleMoveY, this);
            this.handleEndY  = $.proxy(this.handleEndY, this);
            this.initY();

            // Attach Events
            var _this = this;
            this.$window.on('resize.' + this.identifier, debounce(function() {
                // Simulate resizeEnd event.
                delay(function() { _this.updateY(); }, 300);
            }, 20));

            this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDownY);

            // Listen to programmatic value changes
            this.$element.on('change.' + this.identifier, function(e, data) {
                if (data && data.origin === _this.identifier) {
                    return;
                }

                var value = e.target.value,
                    pos = _this.getPositionFromValueY(value);
                _this.setPositionY(pos);
            });

        }

    }

    Plugin.prototype.initX = function() {
        this.updateX(true);

        // Set initial value just in case it is not set already.
        // Prevents trouble if we call `update(true)`
        this.$element[0].value = this.value;

        if (this.onInit && typeof this.onInit === 'function') {
            this.onInit();
        }
    };

    Plugin.prototype.initY = function() {
        this.updateY(true);

        // Set initial value just in case it is not set already.
        // Prevents trouble if we call `update(true)`
        this.$element[0].value = this.value;

        if (this.onInit && typeof this.onInit === 'function') {
            this.onInit();
        }
    };

    Plugin.prototype.updateX = function(updateAttributes) {
        updateAttributes = updateAttributes || false;

        if (updateAttributes) {
            this.min    = parseFloat(this.$element[0].getAttribute('min') || 0);
            this.max    = parseFloat(this.$element[0].getAttribute('max') || 100);
            this.value  = parseFloat(this.$element[0].value || this.min + (this.max-this.min)/2);
            this.step   = parseFloat(this.$element[0].getAttribute('step') || 1);
        }

        this.handleWidth    = getDimension(this.$handle[0], 'offsetWidth');
        this.rangeWidth     = getDimension(this.$range[0], 'offsetWidth');
        this.maxHandleX     = this.rangeWidth - this.handleWidth;
        this.grabX          = this.handleWidth / 2;
        this.position       = this.getPositionFromValueX(this.value);

        // Consider disabled state
        if (this.$element[0].disabled) {
            this.$range.addClass(this.options.disabledClass);
        } else {
            this.$range.removeClass(this.options.disabledClass);
        }

        this.setPositionX(this.position);
    };

    Plugin.prototype.updateY = function(updateAttributes) {
        updateAttributes = updateAttributes || false;

        if (updateAttributes) {
            this.min    = parseFloat(this.$element[0].getAttribute('min') || 0);
            this.max    = parseFloat(this.$element[0].getAttribute('max') || 100);
            this.value  = parseFloat(this.$element[0].value || this.min + (this.max-this.min)/2);
            this.step   = parseFloat(this.$element[0].getAttribute('step') || 1);
        }

        this.handleHeight    = getDimension(this.$handle[0], 'offsetHeight');
        this.rangeHeight     = getDimension(this.$range[0], 'offsetHeight');
        this.maxHandleY     = this.rangeHeight - this.handleHeight;
        this.grabY          = this.handleHeight / 2;
        this.position       = this.getPositionFromValueY(this.value);

        // Consider disabled state
        if (this.$element[0].disabled) {
            this.$range.addClass(this.options.disabledClass);
        } else {
            this.$range.removeClass(this.options.disabledClass);
        }

        this.setPositionY(this.position);
    };

    Plugin.prototype.handleDownX = function(e) {
        e.preventDefault();
        this.$document.on(this.moveEvent, this.handleMoveX);
        this.$document.on(this.endEvent, this.handleEndX);

        // If we click on the handle don't set the new position
        if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
            return;
        }

        var posX    = this.getRelativePositionX(e),
            rangeX  = this.$range[0].getBoundingClientRect().left,
            handleX = this.getPositionFromNode(this.$handle[0]) - rangeX;

            this.setPositionX(posX - this.grabX);

            if (posX >= handleX && posX < handleX + this.handleWidth) {
                this.grabX = posX - handleX;
            }

    };

    Plugin.prototype.handleDownY = function(e) {
        e.preventDefault();
        this.$document.on(this.moveEvent, this.handleMoveY);
        this.$document.on(this.endEvent, this.handleEndY);

        // If we click on the handle don't set the new position
        if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
            return;
        }

        var posY    = this.getRelativePositionY(e),
            rangeY  = this.$range[0].getBoundingClientRect().top,
            handleY = this.getPositionFromNode(this.$handle[0]) - rangeY;

        this.setPositionY(posY - this.grabY);

        if (posY >= handleY && posY < handleY + this.handleHeight) {
            this.grabY = posY - handleY;
        }

    };

    Plugin.prototype.handleMoveX = function(e) {
        e.preventDefault();

        var posX = this.getRelativePositionX(e);

        this.setPositionX(posX - this.grabX);
    };

    Plugin.prototype.handleMoveY = function(e) {
        e.preventDefault();

        var posY = this.getRelativePositionY(e);

        this.setPositionY(posY - this.grabY);
    };

    Plugin.prototype.handleEndX = function(e) {
        e.preventDefault();
        this.$document.off(this.moveEvent, this.handleMoveX);
        this.$document.off(this.endEvent, this.handleEndX);

        // Ok we're done fire the change event
        this.$element.trigger('change', { origin: this.identifier });

        if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
            this.onSlideEnd(this.position, this.value);
        }
    };

    Plugin.prototype.handleEndY = function(e) {
        e.preventDefault();
        this.$document.off(this.moveEvent, this.handleMoveY);
        this.$document.off(this.endEvent, this.handleEndY);

        // Ok we're done fire the change event
        this.$element.trigger('change', { origin: this.identifier });

        if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
            this.onSlideEnd(this.position, this.value);
        }
    };

    Plugin.prototype.cap = function(pos, min, max) {
        if (pos < min) { return min; }
        if (pos > max) { return max; }
        return pos;
    };

    Plugin.prototype.setPositionX = function(pos) {
        var value, left;

        // Snapping steps
        value = this.getValueFromPositionX(this.cap(pos, 0, this.maxHandleX));
        left = this.getPositionFromValueX(value);

        // Update ui
        this.$fill[0].style.width = (left + this.grabX) + 'px';
        this.$handle[0].style.left = left + 'px';
        this.setValue(value);

        // Update globals
        this.position = left;
        this.value = value;

        if (this.onSlide && typeof this.onSlide === 'function') {
            this.onSlide(left, value);
        }

    };

    Plugin.prototype.setPositionY = function(pos) {
        var value, top;

        // Snapping steps
        value = this.getValueFromPositionY(this.cap(pos, 0, this.maxHandleY));
        top = this.getPositionFromValueY(value);

        // Update ui
        this.$fill[0].style.height = (top + this.grabY) + 'px';
        this.$handle[0].style.top = top + 'px';
        this.setValue(value);

        // Update globals
        this.position = top;
        this.value = value;

        if (this.onSlide && typeof this.onSlide === 'function') {
            this.onSlide(top, value);
        }

    };

    // Returns element position relative to the parent
    Plugin.prototype.getPositionFromNode = function(node) {
        var i = 0;
        while (node !== null) {
            i += node.offsetLeft;
            node = node.offsetParent;
        }
        return i;
    };

    Plugin.prototype.getRelativePositionX = function(e) {

        // Get the offset left relative to the viewport
        var rangeX  = this.$range[0].getBoundingClientRect().left,
            pageX   = 0;

        if (typeof e.pageX !== 'undefined') {
            pageX = e.pageX;
        }
        else if (typeof e.originalEvent.clientX !== 'undefined') {
            pageX = e.originalEvent.clientX;
        }
        else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0].clientX !== 'undefined') {
            pageX = e.originalEvent.touches[0].clientX;
        }
        else if(e.currentPoint && typeof e.currentPoint.x !== 'undefined') {
            pageX = e.currentPoint.x;
        }

        return pageX - rangeX;
    };

    Plugin.prototype.getRelativePositionY = function(e, options) {

        // Get the offset top relative to the viewport
        var rangeY  = this.$range[0].getBoundingClientRect().top,
            pageY   = 0;

        if (typeof e.pageY !== 'undefined') {
            pageY = e.pageY;
        }
        else if (typeof e.originalEvent.clientY !== 'undefined') {
            pageY = e.originalEvent.clientY;
        }
        else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0].clientY !== 'undefined') {
            pageY = e.originalEvent.touches[0].clientX;
        }
        else if(e.currentPoint && typeof e.currentPoint.y !== 'undefined') {
            pageY = e.currentPoint.y;
        }

        return pageY - rangeY;
    };

    Plugin.prototype.getPositionFromValueX = function(value) {
        var percentage, pos;

        percentage = (value - this.min)/(this.max - this.min);

        pos = percentage * this.maxHandleX;

        return pos;
    };

    Plugin.prototype.getPositionFromValueY = function(value) {
        var percentage, pos;

        percentage = (value - this.min)/(this.max - this.min);

        pos = percentage * this.maxHandleY;

        return pos;
    };

    Plugin.prototype.getValueFromPositionX = function(pos) {
        var percentage, value;

        percentage = ((pos) / (this.maxHandleX || 1));

        value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
        return Number((value).toFixed(this.toFixed));
    };

    Plugin.prototype.getValueFromPositionY = function(pos) {
        var percentage, value;

        percentage = ((pos) / (this.maxHandleY || 1));

        value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
        return Number((value).toFixed(this.toFixed));
    };

    Plugin.prototype.setValue = function(value) {
        if (value === this.value) {
            return;
        }

        // Set the new value and fire the `input` event
        this.$element
            .val(value)
            .trigger('input', { origin: this.identifier });
    };

    Plugin.prototype.destroy = function() {
        this.$document.off('.' + this.identifier);
        this.$window.off('.' + this.identifier);

        this.$element
            .off('.' + this.identifier)
            .removeAttr('style')
            .removeData('plugin_' + pluginName);

        // Remove the generated markup
        if (this.$range && this.$range.length) {
            this.$range[0].parentNode.removeChild(this.$range[0]);
        }
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[pluginName] = function(options) {
        var args = Array.prototype.slice.call(arguments, 1);

        return this.each(function() {
            var $this = $(this),
                data  = $this.data('plugin_' + pluginName);

            // Create a new instance.
            if (!data) {
                $this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
            }

            // Make it possible to access methods from public.
            // e.g `$element.rangeslider('method');`
            if (typeof options === 'string') {
                data[options].apply(data, args);
            }
        });
    };

}));
chriscamplin commented 9 years ago

please let me know if it's worth a fork

andreruffert commented 9 years ago

@beard86 thanks man! It's definitely worth a fork/pr but what I don't like about your current solution is that it's a lot of duplicate code/methods.

e.g. getValueFromPositionX and getValueFromPositionY have the same functionality expect of some minor changes. Feel free to send a pr after some refactoring to keep the code DRY. :v:

chriscamplin commented 9 years ago

great, thanks for doing that, I started to work on it, but your solution is great.