/** * jQuery Bar Rating Plugin v1.2.2 * * http://github.com/antennaio/jquery-bar-rating * * Copyright (c) 2012-2016 Kazik Pietruszewski * * This plugin is available under the MIT license. * http://www.opensource.org/licenses/mit-license.php */ (function (factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // Node/CommonJS module.exports = factory(require('jquery')); } else { // browser globals factory(jQuery); } }(function ($) { var BarRating = (function() { function BarRating() { var self = this; // wrap element in a wrapper div var wrapElement = function() { var classes = ['br-wrapper']; if (self.options.theme !== '') { classes.push('br-theme-' + self.options.theme); } self.$elem.wrap($('
', { 'class': classes.join(' ') })); }; // unwrap element var unwrapElement = function() { self.$elem.unwrap(); }; // find option by value var findOption = function(value) { if ($.isNumeric(value)) { value = Math.floor(value); } return $('option[value="' + value + '"]', self.$elem); }; // get initial option var getInitialOption = function() { var initialRating = self.options.initialRating; if (!initialRating) { return $('option:selected', self.$elem); } return findOption(initialRating); }; // get empty option var getEmptyOption = function() { var $emptyOpt = self.$elem.find('option[value="' + self.options.emptyValue + '"]'); if (!$emptyOpt.length && self.options.allowEmpty) { $emptyOpt = $('', { 'value': self.options.emptyValue }); return $emptyOpt.prependTo(self.$elem); } return $emptyOpt; }; // get data var getData = function(key) { var data = self.$elem.data('barrating'); if (typeof key !== 'undefined') { return data[key]; } return data; }; // set data var setData = function(key, value) { if (value !== null && typeof value === 'object') { self.$elem.data('barrating', value); } else { self.$elem.data('barrating')[key] = value; } }; // save data on element var saveDataOnElement = function() { var $opt = getInitialOption(); var $emptyOpt = getEmptyOption(); var value = $opt.val(); var text = $opt.data('html') ? $opt.data('html') : $opt.text(); // if the allowEmpty option is not set let's check if empty option exists in the select field var allowEmpty = (self.options.allowEmpty !== null) ? self.options.allowEmpty : !!$emptyOpt.length; var emptyValue = ($emptyOpt.length) ? $emptyOpt.val() : null; var emptyText = ($emptyOpt.length) ? $emptyOpt.text() : null; setData(null, { userOptions: self.options, // initial rating based on the OPTION value ratingValue: value, ratingText: text, // rating will be restored by calling clear method originalRatingValue: value, originalRatingText: text, // allow empty ratings? allowEmpty: allowEmpty, // rating value and text of the empty OPTION emptyRatingValue: emptyValue, emptyRatingText: emptyText, // read-only state readOnly: self.options.readonly, // did the user already select a rating? ratingMade: false }); }; // remove data on element var removeDataOnElement = function() { self.$elem.removeData('barrating'); }; // return current rating text var ratingText = function() { return getData('ratingText'); }; // return current rating value var ratingValue = function() { return getData('ratingValue'); }; // build widget and return jQuery element var buildWidget = function() { var $w = $('', { 'class': 'br-widget' }); // create A elements that will replace OPTIONs self.$elem.find('option').each(function() { var val, text, html, $a; val = $(this).val(); // create ratings - but only if val is not defined as empty if (val !== getData('emptyRatingValue')) { text = $(this).text(); html = $(this).data('html'); if (html) { text = html; } $a = $('', { 'href': '#', 'data-rating-value': val, 'data-rating-text': text, 'html': (self.options.showValues) ? text : '' }); $w.append($a); } }); // append .br-current-rating div to the widget if (self.options.showSelectedRating) { $w.append($('', { 'text': '', 'class': 'br-current-rating' })); } // additional classes for the widget if (self.options.reverse) { $w.addClass('br-reverse'); } if (self.options.readonly) { $w.addClass('br-readonly'); } return $w; }; // return a jQuery function name depending on the 'reverse' setting var nextAllorPreviousAll = function() { if (getData('userOptions').reverse) { return 'nextAll'; } else { return 'prevAll'; } }; // set the value of the select field var setSelectFieldValue = function(value) { // change selected option findOption(value).prop('selected', true); self.$elem.change(); }; // reset select field var resetSelectField = function() { $('option', self.$elem).prop('selected', function() { return this.defaultSelected; }); self.$elem.change(); }; // display the currently selected rating var showSelectedRating = function(text) { // text undefined? text = text ? text : ratingText(); // special case when the selected rating is defined as empty if (text == getData('emptyRatingText')) { text = ''; } // update .br-current-rating div if (self.options.showSelectedRating) { self.$elem.parent().find('.br-current-rating').text(text); } }; // return rounded fraction of a value (14.4 -> 40, 0.99 -> 90) var fraction = function(value) { return Math.round(((Math.floor(value * 10) / 10) % 1) * 100); }; // remove all classes from elements var resetStyle = function() { // remove all classes starting with br-* self.$widget.find('a').removeClass(function(index, classes) { return (classes.match(/(^|\s)br-\S+/g) || []).join(' '); }); }; // apply style by setting classes on elements var applyStyle = function() { var $a = self.$widget.find('a[data-rating-value="' + ratingValue() + '"]'); var initialRating = getData('userOptions').initialRating; var baseValue = $.isNumeric(ratingValue()) ? ratingValue() : 0; var f = fraction(initialRating); var $all, $fractional; resetStyle(); // add classes $a.addClass('br-selected br-current')[nextAllorPreviousAll()]() .addClass('br-selected'); if (!getData('ratingMade') && $.isNumeric(initialRating)) { if ((initialRating <= baseValue) || !f) { return; } $all = self.$widget.find('a'); $fractional = ($a.length) ? $a[(getData('userOptions').reverse) ? 'prev' : 'next']() : $all[(getData('userOptions').reverse) ? 'last' : 'first'](); $fractional.addClass('br-fractional'); $fractional.addClass('br-fractional-' + f); } }; // check if the element is deselectable? var isDeselectable = function($element) { if (!getData('allowEmpty') || !getData('userOptions').deselectable) { return false; } return (ratingValue() == $element.attr('data-rating-value')); }; // handle click events var attachClickHandler = function($elements) { $elements.on('click.barrating', function(event) { var $a = $(this), options = getData('userOptions'), value, text; event.preventDefault(); value = $a.attr('data-rating-value'); text = $a.attr('data-rating-text'); // is current and deselectable? if (isDeselectable($a)) { value = getData('emptyRatingValue'); text = getData('emptyRatingText'); } // remember selected rating setData('ratingValue', value); setData('ratingText', text); setData('ratingMade', true); setSelectFieldValue(value); showSelectedRating(text); applyStyle(); // onSelect callback options.onSelect.call( self, ratingValue(), ratingText(), event ); return false; }); }; // handle mouseenter events var attachMouseEnterHandler = function($elements) { $elements.on('mouseenter.barrating', function() { var $a = $(this); resetStyle(); $a.addClass('br-active')[nextAllorPreviousAll()]() .addClass('br-active'); showSelectedRating($a.attr('data-rating-text')); }); }; // handle mouseleave events var attachMouseLeaveHandler = function($elements) { self.$widget.on('mouseleave.barrating blur.barrating', function() { showSelectedRating(); applyStyle(); }); }; // somewhat primitive way to remove 300ms click delay on touch devices // for a more advanced solution consider setting `fastClicks` option to false // and using a library such as fastclick (https://github.com/ftlabs/fastclick) var fastClicks = function($elements) { $elements.on('touchstart.barrating', function(event) { event.preventDefault(); event.stopPropagation(); $(this).click(); }); }; // disable clicks var disableClicks = function($elements) { $elements.on('click.barrating', function(event) { event.preventDefault(); }); }; var attachHandlers = function($elements) { // attach click event handler attachClickHandler($elements); if (self.options.hoverState) { // attach mouseenter event handler attachMouseEnterHandler($elements); // attach mouseleave event handler attachMouseLeaveHandler($elements); } }; var detachHandlers = function($elements) { // remove event handlers in the ".barrating" namespace $elements.off('.barrating'); }; var setupHandlers = function(readonly) { var $elements = self.$widget.find('a'); if (fastClicks) { fastClicks($elements); } if (readonly) { detachHandlers($elements); disableClicks($elements); } else { attachHandlers($elements); } }; this.show = function() { // run only once if (getData()) return; // wrap element wrapElement(); // save data saveDataOnElement(); // build & append widget to the DOM self.$widget = buildWidget(); self.$widget.insertAfter(self.$elem); applyStyle(); showSelectedRating(); setupHandlers(self.options.readonly); // hide the select field self.$elem.hide(); }; this.readonly = function(state) { if (typeof state !== 'boolean' || getData('readOnly') == state) return; setupHandlers(state); setData('readOnly', state); self.$widget.toggleClass('br-readonly'); }; this.set = function(value) { var options = getData('userOptions'); if (self.$elem.find('option[value="' + value + '"]').length === 0) return; // set data setData('ratingValue', value); setData('ratingText', self.$elem.find('option[value="' + value + '"]').text()); setData('ratingMade', true); setSelectFieldValue(ratingValue()); showSelectedRating(ratingText()); applyStyle(); // onSelect callback if (!options.silent) { options.onSelect.call( this, ratingValue(), ratingText() ); } }; this.clear = function() { var options = getData('userOptions'); // restore original data setData('ratingValue', getData('originalRatingValue')); setData('ratingText', getData('originalRatingText')); setData('ratingMade', false); resetSelectField(); showSelectedRating(ratingText()); applyStyle(); // onClear callback options.onClear.call( this, ratingValue(), ratingText() ); }; this.destroy = function() { var value = ratingValue(); var text = ratingText(); var options = getData('userOptions'); // detach handlers detachHandlers(self.$widget.find('a')); // remove widget self.$widget.remove(); // remove data removeDataOnElement(); // unwrap the element unwrapElement(); // show the element self.$elem.show(); // onDestroy callback options.onDestroy.call( this, value, text ); }; } BarRating.prototype.init = function (options, elem) { this.$elem = $(elem); this.options = $.extend({}, $.fn.barrating.defaults, options); return this.options; }; return BarRating; })(); $.fn.barrating = function (method, options) { return this.each(function () { var plugin = new BarRating(); // plugin works with select fields if (!$(this).is('select')) { $.error('Sorry, this plugin only works with select fields.'); } // method supplied if (plugin.hasOwnProperty(method)) { plugin.init(options, this); if (method === 'show') { return plugin.show(options); } else { // plugin exists? if (plugin.$elem.data('barrating')) { plugin.$widget = $(this).next('.br-widget'); return plugin[method](options); } } // no method supplied or only options supplied } else if (typeof method === 'object' || !method) { options = method; plugin.init(options, this); return plugin.show(); } else { $.error('Method ' + method + ' does not exist on jQuery.barrating'); } }); }; $.fn.barrating.defaults = { theme:'', initialRating:null, // initial rating allowEmpty:null, // allow empty ratings? emptyValue:'', // this is the expected value of the empty rating showValues:false, // display rating values on the bars? showSelectedRating:true, // append a div with a rating to the widget? deselectable:true, // allow to deselect ratings? reverse:false, // reverse the rating? readonly:false, // make the rating ready-only? fastClicks:true, // remove 300ms click delay on touch devices? hoverState:true, // change state on hover? silent:false, // supress callbacks when controlling ratings programatically onSelect:function (value, text, event) { }, // callback fired when a rating is selected onClear:function (value, text) { }, // callback fired when a rating is cleared onDestroy:function (value, text) { } // callback fired when a widget is destroyed }; $.fn.barrating.BarRating = BarRating; }));