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\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.