2
\$\begingroup\$

In need of making a range selecting feature I made my first jQuery plugin. I tried to mimic the structure from some open-source plugins. I wonder if this can be improved.

Here is the source code:

(function($, window, document, undefined) {
    "use strict";

    var _instances = {};
    var _instancesCount = 0;
    var pluginName = 'range-selector';
    var _defaultOptions = {
        min: 0,
        max: 100,
        step: 1,
        value: 0,
        rounding: 0, // Not settable via initializing
        onChange: null,
        clamp: false
    };

    function RangeSelector(element, _opt) {
        this.element = $(element);
        this.options = $.extend({}, _defaultOptions, _opt);
        this.options.rounding = decimalPlaces(this.options.step);
        this.options.value = clamp(this.options.value, this.options.min, this.options.max);

        this.subtract = $('<span>').attr('class', 'sub').html('-');
        this.counter = $('<span>').html(this.options.value);
        this.add = $('<span>').attr('class', 'add').html('+');

        this.element.append(this.subtract, this.counter, this.add);

        this.add.on('click', this.inc.bind(this));
        this.subtract.on('click', this.dec.bind(this));

        this.onValueChange();
    }

    /**
     * https://stackoverflow.com/a/10454560
     */
    function decimalPlaces(num) {
        var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
        if (!match) { return 0; }
        return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
    }

    function clamp(value, min, max) {
        return Math.max(min, Math.min(value, max));
    }

    RangeSelector.prototype.onValueChange = function() {
        if (this.options.value === this.options.min || (!this.options.clamp && this.options.value < this.options.min + this.options.step)) {
            this.subtract.removeClass('active');
        } else {
            this.subtract.addClass('active');
        }

        if (this.options.value === this.options.max || (!this.options.clamp && this.options.value > this.options.max - this.options.step)) {
            this.add.removeClass('active');
        } else {
            this.add.addClass('active');
        }
    };

    RangeSelector.prototype.setValue = function(value) {
        if (this.options.clamp) {
            value = clamp(value, this.options.min, this.options.max);
        }

        if (value >= this.options.min && value <= this.options.max) {
            this.options.value = +value.toFixed(this.options.rounding);
            this.counter.html(this.options.value);
            if (this.options.onChange) {
                this.options.onChange({
                    value: value
                });
            }
        }

        this.onValueChange();
    };

    RangeSelector.prototype.getValue = function() {
        return this.options.value;
    };

    RangeSelector.prototype.inc = function() {
        this.setValue(this.options.value + this.options.step);
    };

    RangeSelector.prototype.dec = function() {
        this.setValue(this.options.value - this.options.step);
    };

    $.fn.rangeSelector = function(_options) {
        var pluginId = $(this).data(pluginName);
        if (pluginId) {
            if (_instances[pluginId] instanceof RangeSelector) {
                if (_options) {
                    _instances[pluginId] = new RangeSelector(this, _options);
                }
                return _instances[pluginId];
            }
        }
        pluginId = 'id' + _instancesCount;
        $(this).data(pluginName, pluginId);
        _instances[pluginId] = new RangeSelector(this, _options);
        _instancesCount++;

        return _instances[pluginId];
    };
})(jQuery, window, document);

Following with a few examples in actions:

(function($, window, document, undefined) {
    "use strict";

    var _instances = {};
    var _instancesCount = 0;
    var pluginName = 'range-selector';
    var _defaultOptions = {
        min: 0,
        max: 100,
        step: 1,
        value: 0,
        rounding: 0, // Not settable via initializing
        onChange: null,
        clamp: false
    };

    function RangeSelector(element, _opt) {
        this.element = $(element);
        this.element.html('');
        this.options = $.extend({}, _defaultOptions, _opt);
        this.options.rounding = decimalPlaces(this.options.step);
        this.options.value = clamp(this.options.value, this.options.min, this.options.max);
        this._name = pluginName;

        this.subtract = $('<span>').attr('class', 'sub').html('-');
        this.counter = $('<span>').attr('class', 'counter').html(this.options.value);
        this.add = $('<span>').attr('class', 'add').html('+');

        this.element.append(this.subtract, this.counter, this.add);

        this.add.on('click', this.inc.bind(this));
        this.subtract.on('click', this.dec.bind(this));

        this.onValueChange();
    }

    /**
     * https://stackoverflow.com/a/10454560
     */
    function decimalPlaces(num) {
        var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
        if (!match) { return 0; }
        return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
    }

    function clamp(value, min, max) {
        return Math.max(min, Math.min(value, max));
    }

    RangeSelector.prototype.onValueChange = function() {
        if (this.options.value === this.options.min || (!this.options.clamp && this.options.value < this.options.min + this.options.step)) {
            this.subtract.removeClass('active');
        } else {
            this.subtract.addClass('active');
        }

        if (this.options.value === this.options.max || (!this.options.clamp && this.options.value > this.options.max - this.options.step)) {
            this.add.removeClass('active');
        } else {
            this.add.addClass('active');
        }
    };

    RangeSelector.prototype.setValue = function(value) {
        if (this.options.clamp) {
            value = clamp(value, this.options.min, this.options.max);
        }

        if (value >= this.options.min && value <= this.options.max) {
            this.options.value = +value.toFixed(this.options.rounding);
            this.counter.html(this.options.value);
            if (this.options.onChange) {
                this.options.onChange({
                    value: value
                });
            }
        }

        this.onValueChange();
    };

    RangeSelector.prototype.getValue = function() {
        return this.options.value;
    };

    RangeSelector.prototype.inc = function() {
        this.setValue(this.options.value + this.options.step);
    };

    RangeSelector.prototype.dec = function() {
        this.setValue(this.options.value - this.options.step);
    };

    $.fn.rangeSelector = function(_options) {
        var pluginId = $(this).data(pluginName);
        if (pluginId) {
            if (_instances[pluginId] instanceof RangeSelector) {
                if (_options) {
                    _instances[pluginId] = new RangeSelector(this, _options);
                }
                return _instances[pluginId];
            }
        }
        pluginId = 'id' + _instancesCount;
        $(this).data(pluginName, pluginId);
        _instances[pluginId] = new RangeSelector(this, _options);
        _instancesCount++;

        return _instances[pluginId];
    };
})(jQuery, window, document);

$('#test1').rangeSelector();
$('#test2').rangeSelector({
  min: 0.5,
  max: 2.5,
  value: 0.6,
  step: 0.45
});
$('#test3').rangeSelector({
  min: 0.5,
  max: 2.5,
  value: 0.6,
  step: 0.45,
  clamp: true
});
.range-selector {

}

.range-selector > .counter {
  width: 40px;
  display: inline-block;
  text-align: center;
}

.range-selector > .sub:not(.active),
.range-selector > .add:not(.active) {
    color: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="test1" class="range-selector"></div>
<div id="test2" class="range-selector"></div>
<div id="test3" class="range-selector"></div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.