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>