/***** * rateYo - v2.3.2 * http://prrashi.github.io/rateyo/ * Copyright (c) 2014 Prashanth Pamidi; Licensed MIT *****/ ;(function ($) { "use strict"; // The basic svg string required to generate stars var BASICSTAR = ""+ ""+ ""+ ""; // The Default values of different options available in the Plugin var DEFAULTS = { starWidth : "32px", normalFill: "gray", ratedFill : "#f39c12", numStars : 5, maxValue : 5, precision : 1, rating : 0, fullStar : false, halfStar : false, readOnly : false, spacing : "0px", rtl : false, multiColor: null, onInit : null, onChange : null, onSet : null, starSvg : null }; //Default colors for multi-color rating var MULTICOLOR_OPTIONS = { startColor: "#c0392b", //red endColor : "#f1c40f" //yellow }; // http://stackoverflow.com/questions/11381673/detecting-a-mobile-browser function isMobileBrowser () { var check = false; /* jshint ignore:start */ (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera); /* jshint ignore:end */ return check; } function checkPrecision (value, minValue, maxValue) { /* * This function removes the unnecessary precision, at Min and Max Values */ // Its like comparing 0.0 with 0, which is true if (value === minValue) { value = minValue; } else if(value === maxValue) { value = maxValue; } return value; } function checkBounds (value, minValue, maxValue) { /* * Check if the value is between min and max values, if not, throw an error */ var isValid = value >= minValue && value <= maxValue; if(!isValid){ throw Error("Invalid Rating, expected value between "+ minValue + " and " + maxValue); } return value; } function isDefined(value) { // Better way to check if a variable is defined or not return typeof value !== "undefined"; } // Regex to match Colors in Hex Format like #FF00FF var hexRegex = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i; var hexToRGB = function (hex) { /* * Extracts and returns the Red, Blue, Green Channel values, * in the form of decimals */ if (!hexRegex.test(hex)) { return null; } var hexValues = hexRegex.exec(hex), r = parseInt(hexValues[1], 16), g = parseInt(hexValues[2], 16), b = parseInt(hexValues[3], 16); return {r:r, g:g, b:b}; }; function getChannelValue(startVal, endVal, percent) { /* * Returns a value between `startVal` and `endVal` based on the percent */ var newVal = (endVal - startVal)*(percent/100); newVal = Math.round(startVal + newVal).toString(16); if (newVal.length === 1) { newVal = "0" + newVal; } return newVal; } function getColor (startColor, endColor, percent) { /* * Given the percentage( `percent` ) of `endColor` to be mixed * with the `startColor`, returns the mixed color. * Colors should be only in Hex Format */ if (!startColor || !endColor) { return null; } percent = isDefined(percent)? percent : 0; startColor = hexToRGB(startColor); endColor = hexToRGB(endColor); var r = getChannelValue(startColor.r, endColor.r, percent), b = getChannelValue(startColor.b, endColor.b, percent), g = getChannelValue(startColor.g, endColor.g, percent); return "#" + r + g + b; } function RateYo ($node, options) { /* * The Contructor, whose instances are used by plugin itself */ // Storing the HTML element as a property, for future access this.node = $node.get(0); var that = this; // Remove any stuff that is present inside the container, and add the plugin class $node.empty().addClass("jq-ry-container"); /* * Basically the plugin displays the rating using two rows of stars lying one above * the other, the row that is on the top represents the actual rating, and the one * behind acts just like a background. * * `$groupWrapper`: is an element that wraps both the rows * `$normalGroup`: is the container for row of stars thats behind and * acts as background * `$ratedGroup`: is the container for row of stars that display the actual rating. * * The rating is displayed by adjusting the width of `$ratedGroup` */ var $groupWrapper = $("
").addClass("jq-ry-group-wrapper") .appendTo($node); var $normalGroup = $("
").addClass("jq-ry-normal-group") .addClass("jq-ry-group") .appendTo($groupWrapper); var $ratedGroup = $("
").addClass("jq-ry-rated-group") .addClass("jq-ry-group") .appendTo($groupWrapper); /* * Variable `step`: store the value of the rating for each star * eg: if `maxValue` is 5 and `numStars` is 5, value of each star * is 1. * Variable `starWidth`: stores the decimal value of width of star in units of px * Variable `percentOfStar`: stores the percentage of width each star takes w.r.t * the container * Variable `spacing`: stores the decimal value of the spacing between stars * in the units of px * Variable `percentOfSpacing`: stores the percentage of width of the spacing * between stars w.r.t the container */ var step, starWidth, percentOfStar, spacing, percentOfSpacing, containerWidth, minValue = 0; /* * `currentRating` contains rating that is being displayed at the latest point of * time. * * When ever you hover over the plugin UI, the rating value changes * according to the place where you point the cursor, currentRating contains * the current value of rating that is being shown in the UI */ var currentRating = options.rating; // A flag to store if the plugin is already being displayed in the UI var isInitialized = false; function showRating (ratingVal) { /* * The function is responsible for displaying the rating by changing * the width of `$ratedGroup` */ if (!isDefined(ratingVal)) { ratingVal = options.rating; } // Storing the value that is being shown in `currentRating`. currentRating = ratingVal; var numStarsToShow = ratingVal/step; // calculating the percentage of width of $ratedGroup with respect to its parent var percent = numStarsToShow*percentOfStar; if (numStarsToShow > 1) { // adding the percentage of space that is taken by the gap the stars percent += (Math.ceil(numStarsToShow) - 1)*percentOfSpacing; } setRatedFill(options.ratedFill); percent = options.rtl ? 100 - percent : percent; if (percent < 0) { percent = 0; } else if (percent > 100) { percent = 100; } $ratedGroup.css("width", percent + "%"); } function setContainerWidth () { /* * Set the width of the `this.node` based on the width of each star and * the space between them */ containerWidth = starWidth*options.numStars + spacing*(options.numStars - 1); percentOfStar = (starWidth/containerWidth)*100; percentOfSpacing = (spacing/containerWidth)*100; $node.width(containerWidth); showRating(); } function setStarWidth (newWidth) { /* * Set the width and height of each SVG star, called whenever one changes the * `starWidth` option */ // The width and height of the star should be the same var starHeight = options.starWidth = newWidth; starWidth = window.parseFloat(options.starWidth.replace("px", "")); $normalGroup.find("svg") .attr({width : options.starWidth, height: starHeight}); $ratedGroup.find("svg") .attr({width : options.starWidth, height: starHeight}); setContainerWidth(); return $node; } function setSpacing (newSpacing) { /* * Set spacing between the SVG stars, called whenever one changes * the `spacing` option */ options.spacing = newSpacing; spacing = parseFloat(options.spacing.replace("px", "")); $normalGroup.find("svg:not(:first-child)") .css({"margin-left": newSpacing}); $ratedGroup.find("svg:not(:first-child)") .css({"margin-left": newSpacing}); setContainerWidth(); return $node; } function setNormalFill (newFill) { /* * Set the background fill of the Stars, called whenever one changes the * `normalFill` option */ options.normalFill = newFill; var $svgs = (options.rtl ? $ratedGroup : $normalGroup).find("svg"); $svgs.attr({fill: options.normalFill}); return $node; } /* * Store the recent `ratedFill` option in a variable * so that if multiColor is unset, we can use the perviously set `ratedFill` * from this variable */ var ratedFill = options.ratedFill; function setRatedFill (newFill) { /* * Set ratedFill of the stars, called when one changes the `ratedFill` option */ /* * If `multiColor` option is set, `newFill` variable is dynamically set * based on the rating, what ever set as parameter will be discarded */ if (options.multiColor) { var ratingDiff = currentRating - minValue, percentCovered = (ratingDiff/options.maxValue)*100; var colorOpts = options.multiColor || {}, startColor = colorOpts.startColor || MULTICOLOR_OPTIONS.startColor, endColor = colorOpts.endColor || MULTICOLOR_OPTIONS.endColor; newFill = getColor(startColor, endColor, percentCovered); } else { ratedFill = newFill; } options.ratedFill = newFill; var $svgs = (options.rtl ? $normalGroup : $ratedGroup).find("svg"); $svgs.attr({fill: options.ratedFill}); return $node; } function setRtl (newValue) { newValue = !!newValue; options.rtl = newValue; setNormalFill(options.normalFill); showRating(); } function setMultiColor (colorOptions) { /* * called whenever one changes the `multiColor` option */ options.multiColor = colorOptions; // set the recently set `ratedFill` option, if multiColor Options are unset setRatedFill(colorOptions ? colorOptions : ratedFill); } function setNumStars (newValue) { /* * Set the number of stars to use to display the rating, called whenever one * changes the `numStars` option */ options.numStars = newValue; step = options.maxValue/options.numStars; $normalGroup.empty(); $ratedGroup.empty(); for (var i=0; i newValue) { setRating(newValue); } showRating(); return $node; } function setPrecision (newValue) { /* * Set the precision of the rating value, called if one changes the * `precision` option */ options.precision = newValue; setRating(options.rating); return $node; } function setHalfStar (newValue) { /* * This function will be called if one changes the `halfStar` option */ options.halfStar = newValue; return $node; } function setFullStar (newValue) { /* * This function will be called if one changes the `fullStar` option */ options.fullStar = newValue; return $node; } function round (value) { /* * Rounds the value of rating if `halfStar` or `fullStar` options are chosen */ var remainder = value%step, halfStep = step/2, isHalfStar = options.halfStar, isFullStar = options.fullStar; if (!isFullStar && !isHalfStar) { return value; } if (isFullStar || (isHalfStar && remainder > halfStep)) { value += step - remainder; } else { value = value - remainder; if (remainder > 0) { value += halfStep; } } return value; } function calculateRating (e) { /* * Calculates and returns the rating based on the position of cursor w.r.t the * plugin container */ var position = $normalGroup.offset(), nodeStartX = position.left, nodeEndX = nodeStartX + $normalGroup.width(); var maxValue = options.maxValue; // The x-coordinate(position) of the mouse pointer w.r.t page var pageX = e.pageX; var calculatedRating = 0; // If the mouse pointer is to the left of the container if(pageX < nodeStartX) { calculatedRating = minValue; }else if (pageX > nodeEndX) { // If the mouse pointer is right of the container calculatedRating = maxValue; }else { // If the mouse pointer is inside the continer /* * The fraction of width covered by the pointer w.r.t to the total width * of the container. */ var calcPrcnt = ((pageX - nodeStartX)/(nodeEndX - nodeStartX)); if (spacing > 0) { /* * If there is spacing between stars, take the percentage of width covered * and subtract the percentage of width covered by stars and spacing, to find * how many stars are covered, the number of stars covered is the rating * * TODO: I strongly feel that this logic can be improved!, Please help! */ calcPrcnt *= 100; var remPrcnt = calcPrcnt; while (remPrcnt > 0) { if (remPrcnt > percentOfStar) { calculatedRating += step; remPrcnt -= (percentOfStar + percentOfSpacing); } else { calculatedRating += remPrcnt/percentOfStar*step; remPrcnt = 0; } } } else { /* * If there is not spacing between stars, the fraction of width covered per * `maxValue` is the rating */ calculatedRating = calcPrcnt * (options.maxValue); } // Round the rating if `halfStar` or `fullStar` options are chosen calculatedRating = round(calculatedRating); } if (options.rtl) { calculatedRating = maxValue - calculatedRating; } return parseFloat(calculatedRating); } function setReadOnly (newValue) { /* * UnBinds mouse event handlers, called when whenever one changes the * `readOnly` option */ options.readOnly = newValue; $node.attr("readonly", true); unbindEvents(); if (!newValue) { $node.removeAttr("readonly"); bindEvents(); } return $node; } function setRating (newValue) { /* * Sets the rating of the Plugin, Called when option `rating` is changed * or, when `rating` method is called */ var rating = newValue; var maxValue = options.maxValue; if (typeof rating === "string") { // If rating is given in percentage, maxValue should be 100 if (rating[rating.length - 1] === "%") { rating = rating.substr(0, rating.length - 1); maxValue = 100; setMaxValue(maxValue); } rating = parseFloat(rating); } checkBounds(rating, minValue, maxValue); rating = parseFloat(rating.toFixed(options.precision)); checkPrecision(parseFloat(rating), minValue, maxValue); options.rating = rating; showRating(); if (isInitialized) { $node.trigger("rateyo.set", {rating: rating}); } return $node; } function setOnInit (method) { /* * set what method to be called on Initialization */ options.onInit = method; return $node; } function setOnSet (method) { /* * set what method to be called when rating is set */ options.onSet = method; return $node; } function setOnChange (method) { /* * set what method to be called rating in the UI is changed */ options.onChange = method; return $node; } this.rating = function (newValue) { /* * rating getter/setter */ if (!isDefined(newValue)) { return options.rating; } setRating(newValue); return $node; }; this.destroy = function () { /* * Removes the Rating UI by clearing the content, and removing the custom classes */ if (!options.readOnly) { unbindEvents(); } RateYo.prototype.collection = deleteInstance($node.get(0), this.collection); $node.removeClass("jq-ry-container").children().remove(); return $node; }; this.method = function (methodName) { /* * Method to call the methods of RateYo Instance */ if (!methodName) { throw Error("Method name not specified!"); } if (!isDefined(this[methodName])) { throw Error("Method " + methodName + " doesn't exist!"); } var args = Array.prototype.slice.apply(arguments, []), params = args.slice(1), method = this[methodName]; return method.apply(this, params); }; this.option = function (optionName, param) { /* * Method to get/set Options */ if (!isDefined(optionName)) { return options; } var method; switch (optionName) { case "starWidth": method = setStarWidth; break; case "numStars": method = setNumStars; break; case "normalFill": method = setNormalFill; break; case "ratedFill": method = setRatedFill; break; case "multiColor": method = setMultiColor; break; case "maxValue": method = setMaxValue; break; case "precision": method = setPrecision; break; case "rating": method = setRating; break; case "halfStar": method = setHalfStar; break; case "fullStar": method = setFullStar; break; case "readOnly": method = setReadOnly; break; case "spacing": method = setSpacing; break; case "rtl": method = setRtl; break; case "onInit": method = setOnInit; break; case "onSet": method = setOnSet; break; case "onChange": method = setOnChange; break; default: throw Error("No such option as " + optionName); } return isDefined(param) ? method(param) : options[optionName]; }; function onMouseEnter (e) { /* * If the Mouse Pointer is inside the container, calculate and show the rating * in UI */ var rating = calculateRating(e).toFixed(options.precision); var maxValue = options.maxValue; rating = checkPrecision(parseFloat(rating), minValue, maxValue); showRating(rating); $node.trigger("rateyo.change", {rating: rating}); } function onMouseLeave () { if (isMobileBrowser()) { return; } /* * If mouse leaves, revert the rating in UI to previously set rating, * when empty value is passed to showRating, it will take the previously set * rating */ showRating(); $node.trigger("rateyo.change", {rating: options.rating}); } function onMouseClick (e) { /* * On clicking the mouse inside the container, calculate and set the rating */ var resultantRating = calculateRating(e).toFixed(options.precision); resultantRating = parseFloat(resultantRating); that.rating(resultantRating); } function onInit(e, data) { if(options.onInit && typeof options.onInit === "function") { /* jshint validthis:true */ options.onInit.apply(this, [data.rating, that]); } } function onChange (e, data) { if(options.onChange && typeof options.onChange === "function") { /* jshint validthis:true */ options.onChange.apply(this, [data.rating, that]); } } function onSet (e, data) { if(options.onSet && typeof options.onSet === "function") { /* jshint validthis:true */ options.onSet.apply(this, [data.rating, that]); } } function bindEvents () { $node.on("mousemove", onMouseEnter) .on("mouseenter", onMouseEnter) .on("mouseleave", onMouseLeave) .on("click", onMouseClick) .on("rateyo.init", onInit) .on("rateyo.change", onChange) .on("rateyo.set", onSet); } function unbindEvents () { $node.off("mousemove", onMouseEnter) .off("mouseenter", onMouseEnter) .off("mouseleave", onMouseLeave) .off("click", onMouseClick) .off("rateyo.init", onInit) .off("rateyo.change", onChange) .off("rateyo.set", onSet); } setNumStars(options.numStars); setReadOnly(options.readOnly); if (options.rtl) { setRtl(options.rtl); } this.collection.push(this); this.rating(options.rating, true); isInitialized = true; $node.trigger("rateyo.init", {rating: options.rating}); } RateYo.prototype.collection = []; function getInstance (node, collection) { /* * Given a HTML element (node) and a collection of RateYo instances, * this function will search through the collection and return the matched * instance having the node */ var instance; $.each(collection, function () { if(node === this.node){ instance = this; return false; } }); return instance; } function deleteInstance (node, collection) { /* * Given a HTML element (node) and a collection of RateYo instances, * this function will search through the collection and delete the * instance having the node, and return the modified collection */ $.each(collection, function (index) { if (node === this.node) { var firstPart = collection.slice(0, index), secondPart = collection.slice(index+1, collection.length); collection = firstPart.concat(secondPart); return false; } }); return collection; } function _rateYo (options) { var rateYoInstances = RateYo.prototype.collection; /* jshint validthis:true */ var $nodes = $(this); if($nodes.length === 0) { return $nodes; } var args = Array.prototype.slice.apply(arguments, []); if (args.length === 0) { //If args length is 0, Initialize the UI with default settings options = args[0] = {}; }else if (args.length === 1 && typeof args[0] === "object") { //If an Object is specified as first argument, it is considered as options options = args[0]; }else if (args.length >= 1 && typeof args[0] === "string") { /* * if there is only one argument, and if its a string, it is supposed to be a * method name, if more than one argument is specified, the remaining arguments * except the first argument, will be passed as a params to the specified method */ var methodName = args[0], params = args.slice(1); var result = []; $.each($nodes, function (i, node) { var existingInstance = getInstance(node, rateYoInstances); if(!existingInstance) { throw Error("Trying to set options before even initialization"); } var method = existingInstance[methodName]; if (!method) { throw Error("Method " + methodName + " does not exist!"); } var returnVal = method.apply(existingInstance, params); result.push(returnVal); }); /* * If the plugin in being called on only one jQuery Element, return only the * first value, to support chaining. */ result = result.length === 1? result[0]: result; return result; }else { throw Error("Invalid Arguments"); } /* * if only options are passed, extend default options, and if the plugin is not * initialized on a particular jQuery element, initalize RateYo on it */ options = $.extend({}, DEFAULTS, options); return $.each($nodes, function () { var existingInstance = getInstance(this, rateYoInstances); if (existingInstance) { return existingInstance; } var $node = $(this), dataAttrs = {}, optionsCopy = $.extend({}, options); $.each($node.data(), function (key, value) { if (key.indexOf("rateyo") !== 0) { return; } var optionName = key.replace(/^rateyo/, ""); optionName = optionName[0].toLowerCase() + optionName.slice(1); dataAttrs[optionName] = value; delete optionsCopy[optionName]; }); return new RateYo($(this), $.extend({}, dataAttrs, optionsCopy)); }); } function rateYo () { /* jshint validthis:true */ return _rateYo.apply(this, Array.prototype.slice.apply(arguments, [])); } window.RateYo = RateYo; $.fn.rateYo = rateYo; }(window.jQuery));