jquery.rateit.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /*! RateIt | v1.0.18 / 12/22/2013 | https://rateit.codeplex.com/license
  2. http://rateit.codeplex.com | Twitter: @gjunge
  3. */
  4. (function ($) {
  5. $.rateit = {
  6. aria : {
  7. resetLabel: 'reset rating',
  8. ratingLabel: 'rating'
  9. }
  10. }
  11. $.fn.rateit = function (p1, p2) {
  12. //quick way out.
  13. var index = 1;
  14. var options = {}; var mode = 'init';
  15. var capitaliseFirstLetter = function (string) {
  16. return string.charAt(0).toUpperCase() + string.substr(1);
  17. };
  18. if (this.length == 0) return this;
  19. var tp1 = $.type(p1);
  20. if (tp1 == 'object' || p1 === undefined || p1 == null) {
  21. options = $.extend({}, $.fn.rateit.defaults, p1); //wants to init new rateit plugin(s).
  22. }
  23. else if (tp1 == 'string' && p1 !== 'reset' && p2 === undefined) {
  24. return this.data('rateit' + capitaliseFirstLetter(p1)); //wants to get a value.
  25. }
  26. else if (tp1 == 'string') {
  27. mode = 'setvalue'
  28. }
  29. return this.each(function () {
  30. var item = $(this);
  31. //shorten all the item.data('rateit-XXX'), will save space in closure compiler, will be like item.data('XXX') will become x('XXX')
  32. var itemdata = function (key, value) {
  33. if (value != null) {
  34. //update aria values
  35. var ariakey = 'aria-value' + ((key == 'value') ? 'now' : key);
  36. var range = item.find('.rateit-range');
  37. if (range.attr(ariakey) != undefined) {
  38. range.attr(ariakey, value);
  39. }
  40. }
  41. arguments[0] = 'rateit' + capitaliseFirstLetter(key);
  42. return item.data.apply(item, arguments); ////Fix for WI: 523
  43. };
  44. //handle programmatic reset
  45. if (p1 == 'reset') {
  46. var setup = itemdata('init'); //get initial value
  47. for (var prop in setup) {
  48. item.data(prop, setup[prop]);
  49. }
  50. if (itemdata('backingfld')) { //reset also backingfield
  51. var fld = $(itemdata('backingfld'));
  52. fld.val(itemdata('value'));
  53. if (fld[0].min) fld[0].min = itemdata('min');
  54. if (fld[0].max) fld[0].max = itemdata('max');
  55. if (fld[0].step) fld[0].step = itemdata('step');
  56. }
  57. item.trigger('reset');
  58. }
  59. //add the rate it class.
  60. if (!item.hasClass('rateit')) item.addClass('rateit');
  61. var ltr = item.css('direction') != 'rtl';
  62. // set value mode
  63. if (mode == 'setvalue') {
  64. if (!itemdata('init')) throw 'Can\'t set value before init';
  65. //if readonly now and it wasn't readonly, remove the eventhandlers.
  66. if (p1 == 'readonly' && p2 == true && !itemdata('readonly')) {
  67. item.find('.rateit-range').unbind();
  68. itemdata('wired', false);
  69. }
  70. //when we receive a null value, reset the score to its min value.
  71. if (p1 == 'value')
  72. p2 = (p2 == null) ? itemdata('min') : Math.max(itemdata('min'), Math.min(itemdata('max'), p2));
  73. if (itemdata('backingfld')) {
  74. //if we have a backing field, check which fields we should update.
  75. //In case of input[type=range], although we did read its attributes even in browsers that don't support it (using fld.attr())
  76. //we only update it in browser that support it (&& fld[0].min only works in supporting browsers), not only does it save us from checking if it is range input type, it also is unnecessary.
  77. var fld = $(itemdata('backingfld'));
  78. if (p1 == 'value') fld.val(p2);
  79. if (p1 == 'min' && fld[0].min) fld[0].min = p2;
  80. if (p1 == 'max' && fld[0].max) fld[0].max = p2;
  81. if (p1 == 'step' && fld[0].step) fld[0].step = p2;
  82. }
  83. itemdata(p1, p2);
  84. }
  85. //init rateit plugin
  86. if (!itemdata('init')) {
  87. //get our values, either from the data-* html5 attribute or from the options.
  88. itemdata('min', isNaN(itemdata('min')) ? options.min : itemdata('min'));
  89. itemdata('max', isNaN(itemdata('max')) ? options.max : itemdata('max'));
  90. itemdata('step', itemdata('step') || options.step);
  91. itemdata('readonly', itemdata('readonly') !== undefined ? itemdata('readonly') : options.readonly);
  92. itemdata('resetable', itemdata('resetable') !== undefined ? itemdata('resetable') : options.resetable);
  93. itemdata('backingfld', itemdata('backingfld') || options.backingfld);
  94. itemdata('starwidth', itemdata('starwidth') || options.starwidth);
  95. itemdata('starheight', itemdata('starheight') || options.starheight);
  96. itemdata('value', Math.max(itemdata('min'), Math.min(itemdata('max'), (!isNaN(itemdata('value')) ? itemdata('value') : (!isNaN(options.value) ? options.value : options.min) ))));
  97. itemdata('ispreset', itemdata('ispreset') !== undefined ? itemdata('ispreset') : options.ispreset);
  98. //are we LTR or RTL?
  99. if (itemdata('backingfld')) {
  100. //if we have a backing field, hide it, and get its value, and override defaults if range.
  101. var fld = $(itemdata('backingfld'));
  102. itemdata('value', fld.hide().val());
  103. if (fld.attr('disabled') || fld.attr('readonly'))
  104. itemdata('readonly', true); //http://rateit.codeplex.com/discussions/362055 , if a backing field is disabled or readonly at instantiation, make rateit readonly.
  105. if (fld[0].nodeName == 'INPUT') {
  106. if (fld[0].type == 'range' || fld[0].type == 'text') { //in browsers not support the range type, it defaults to text
  107. itemdata('min', parseInt(fld.attr('min')) || itemdata('min')); //if we would have done fld[0].min it wouldn't have worked in browsers not supporting the range type.
  108. itemdata('max', parseInt(fld.attr('max')) || itemdata('max'));
  109. itemdata('step', parseInt(fld.attr('step')) || itemdata('step'));
  110. }
  111. }
  112. if (fld[0].nodeName == 'SELECT' && fld[0].options.length > 1) {
  113. itemdata('min', Number(fld[0].options[0].value));
  114. itemdata('max', Number(fld[0].options[fld[0].length - 1].value));
  115. itemdata('step', Number(fld[0].options[1].value) - Number(fld[0].options[0].value));
  116. }
  117. }
  118. //Create the necessary tags. For ARIA purposes we need to give the items an ID. So we use an internal index to create unique ids
  119. var element = item[0].nodeName == 'DIV' ? 'div' : 'span';
  120. index++;
  121. var html = '<button id="rateit-reset-{{index}}" data-role="none" class="rateit-reset" aria-label="' + $.rateit.aria.resetLabel + '" aria-controls="rateit-range-{{index}}"></button><{{element}} id="rateit-range-{{index}}" class="rateit-range" tabindex="0" role="slider" aria-label="' + $.rateit.aria.ratingLabel + '" aria-owns="rateit-reset-{{index}}" aria-valuemin="' + itemdata('min') + '" aria-valuemax="' + itemdata('max') + '" aria-valuenow="' + itemdata('value') + '"><{{element}} class="rateit-selected" style="height:' + itemdata('starheight') + 'px"></{{element}}><{{element}} class="rateit-hover" style="height:' + itemdata('starheight') + 'px"></{{element}}></{{element}}>';
  122. item.append(html.replace(/{{index}}/gi, index).replace(/{{element}}/gi, element));
  123. //if we are in RTL mode, we have to change the float of the "reset button"
  124. if (!ltr) {
  125. item.find('.rateit-reset').css('float', 'right');
  126. item.find('.rateit-selected').addClass('rateit-selected-rtl');
  127. item.find('.rateit-hover').addClass('rateit-hover-rtl');
  128. }
  129. itemdata('init', JSON.parse(JSON.stringify(item.data()))); //cheap way to create a clone
  130. }
  131. //resize the height of all elements,
  132. item.find('.rateit-selected, .rateit-hover').height(itemdata('starheight'));
  133. //set the range element to fit all the stars.
  134. var range = item.find('.rateit-range');
  135. range.width(itemdata('starwidth') * (itemdata('max') - itemdata('min'))).height(itemdata('starheight'));
  136. //add/remove the preset class
  137. var presetclass = 'rateit-preset' + ((ltr) ? '' : '-rtl');
  138. if (itemdata('ispreset'))
  139. item.find('.rateit-selected').addClass(presetclass);
  140. else
  141. item.find('.rateit-selected').removeClass(presetclass);
  142. //set the value if we have it.
  143. if (itemdata('value') != null) {
  144. var score = (itemdata('value') - itemdata('min')) * itemdata('starwidth');
  145. item.find('.rateit-selected').width(score);
  146. }
  147. //setup the reset button
  148. var resetbtn = item.find('.rateit-reset');
  149. if (resetbtn.data('wired') !== true) {
  150. resetbtn.bind('click', function (e) {
  151. e.preventDefault();
  152. resetbtn.blur();
  153. item.rateit('value', null);
  154. item.trigger('reset');
  155. }).data('wired', true);
  156. }
  157. //this function calculates the score based on the current position of the mouse.
  158. var calcRawScore = function (element, event) {
  159. var pageX = (event.changedTouches) ? event.changedTouches[0].pageX : event.pageX;
  160. var offsetx = pageX - $(element).offset().left;
  161. if (!ltr) offsetx = range.width() - offsetx;
  162. if (offsetx > range.width()) offsetx = range.width();
  163. if (offsetx < 0) offsetx = 0;
  164. return score = Math.ceil(offsetx / itemdata('starwidth') * (1 / itemdata('step')));
  165. };
  166. //sets the hover element based on the score.
  167. var setHover = function (score) {
  168. var w = score * itemdata('starwidth') * itemdata('step');
  169. var h = range.find('.rateit-hover');
  170. if (h.data('width') != w) {
  171. range.find('.rateit-selected').hide();
  172. h.width(w).show().data('width', w);
  173. var data = [(score * itemdata('step')) + itemdata('min')];
  174. item.trigger('hover', data).trigger('over', data);
  175. }
  176. };
  177. var setSelection = function (value) {
  178. itemdata('value', value);
  179. if (itemdata('backingfld')) {
  180. $(itemdata('backingfld')).val(value);
  181. }
  182. if (itemdata('ispreset')) { //if it was a preset value, unset that.
  183. range.find('.rateit-selected').removeClass(presetclass);
  184. itemdata('ispreset', false);
  185. }
  186. range.find('.rateit-hover').hide();
  187. range.find('.rateit-selected').width(value * itemdata('starwidth') - (itemdata('min') * itemdata('starwidth'))).show();
  188. item.trigger('hover', [null]).trigger('over', [null]).trigger('rated', [value]);
  189. };
  190. if (!itemdata('readonly')) {
  191. //if we are not read only, add all the events
  192. //if we have a reset button, set the event handler.
  193. if (!itemdata('resetable'))
  194. resetbtn.hide();
  195. //when the mouse goes over the range element, we set the "hover" stars.
  196. if (!itemdata('wired')) {
  197. range.bind('touchmove touchend', touchHandler); //bind touch events
  198. range.mousemove(function (e) {
  199. var score = calcRawScore(this, e);
  200. setHover(score);
  201. });
  202. //when the mouse leaves the range, we have to hide the hover stars, and show the current value.
  203. range.mouseleave(function (e) {
  204. range.find('.rateit-hover').hide().width(0).data('width', '');
  205. item.trigger('hover', [null]).trigger('over', [null]);
  206. range.find('.rateit-selected').show();
  207. });
  208. //when we click on the range, we have to set the value, hide the hover.
  209. range.mouseup(function (e) {
  210. var score = calcRawScore(this, e);
  211. var value = (score * itemdata('step')) + itemdata('min');
  212. setSelection(value);
  213. range.blur();
  214. });
  215. //support key nav
  216. range.keyup( function (e) {
  217. if (e.which == 38 || e.which == (ltr ? 39 : 37)) {
  218. setSelection(Math.min(itemdata('value') + itemdata('step'), itemdata('max')));
  219. }
  220. if (e.which == 40 || e.which == (ltr ? 37 : 39)) {
  221. setSelection(Math.max(itemdata('value') - itemdata('step'), itemdata('min')));
  222. }
  223. });
  224. itemdata('wired', true);
  225. }
  226. if (itemdata('resetable')) {
  227. resetbtn.show();
  228. }
  229. }
  230. else {
  231. resetbtn.hide();
  232. }
  233. range.attr('aria-readonly', itemdata('readonly'));
  234. });
  235. };
  236. //touch converter http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/
  237. function touchHandler(event) {
  238. var touches = event.originalEvent.changedTouches,
  239. first = touches[0],
  240. type = "";
  241. switch (event.type) {
  242. case "touchmove": type = "mousemove"; break;
  243. case "touchend": type = "mouseup"; break;
  244. default: return;
  245. }
  246. var simulatedEvent = document.createEvent("MouseEvent");
  247. simulatedEvent.initMouseEvent(type, true, true, window, 1,
  248. first.screenX, first.screenY,
  249. first.clientX, first.clientY, false,
  250. false, false, false, 0/*left*/, null);
  251. first.target.dispatchEvent(simulatedEvent);
  252. event.preventDefault();
  253. };
  254. //some default values.
  255. $.fn.rateit.defaults = { min: 0, max: 5, step: 0.5, starwidth: 16, starheight: 16, readonly: false, resetable: true, ispreset: false};
  256. //invoke it on all .rateit elements. This could be removed if not wanted.
  257. $(function () { $('div.rateit, span.rateit').rateit(); });
  258. })(jQuery);