word-and-character-counter.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /*! word-and-character-counter.js
  2. v2.4 (c) Wilkins Fernandez
  3. MIT License
  4. */
  5. (function($) {
  6. $.fn.extend({
  7. counter: function(options) {
  8. var defaults = {
  9. // {char || word}
  10. type: 'char',
  11. // count {up || down} from or to the goal number
  12. count: 'down',
  13. // count {to || from} this number
  14. goal: 140,
  15. // Show description of counter
  16. text: true,
  17. // Specify target for the counter
  18. target: false,
  19. // Append target, otherwise prepend
  20. append: true,
  21. // Provide translate text for counter message
  22. translation: '',
  23. // Custom counter message
  24. msg: ''
  25. };
  26. var $countObj = '',
  27. countIndex = '',
  28. noLimit = false,
  29. // Pass {} as first argument to preserve defaults/options for comparision
  30. options = $.extend({}, defaults, options);
  31. // Adds the counter to the page and binds counter to user input fields
  32. var methods = {
  33. init: function($obj) {
  34. var objID = $obj.attr('id'),
  35. counterID = objID + '_count';
  36. // Check if unlimited typing is enabled
  37. methods.isLimitless();
  38. // Insert counter after or before text area/box
  39. $countObj = $("<span id=" + counterID + "/>");
  40. var counterDiv = $('<div/>').attr('id', objID + '_counter').addClass( 'input-counter' ).append($countObj).append(" " + methods.setMsg());
  41. if(!options.target || !$(options.target).length) { //target is not specified or invalid
  42. options.append ? counterDiv.insertAfter($obj) : counterDiv.insertBefore($obj);
  43. }
  44. else { // append/prepend counter to specified target
  45. options.append ? $(options.target).append(counterDiv) : $(options.target).prepend(counterDiv);
  46. }
  47. // Bind methods to events
  48. methods.bind($obj);
  49. },
  50. // Bind everything!
  51. bind: function($obj) {
  52. $obj.bind("keypress.counter keydown.counter keyup.counter blur.counter focus.counter change.counter paste.counter", methods.updateCounter);
  53. $obj.bind("keydown.counter", methods.doStopTyping);
  54. $obj.trigger('keydown');
  55. },
  56. // Enables uninterrupted typing (just counting)
  57. isLimitless: function() {
  58. if(options.goal === 'sky') {
  59. // Override to count up
  60. options.count = 'up';
  61. // methods.isGoalReached will always return false
  62. noLimit = true;
  63. return noLimit;
  64. }
  65. },
  66. /* Sets the appropriate message after counter */
  67. setMsg: function() {
  68. // Show custom message
  69. if(options.msg !== '') {
  70. return options.msg;
  71. }
  72. // Show no message
  73. if(options.text === false) {
  74. return '';
  75. }
  76. // Only show custom message if there is one
  77. if(noLimit) {
  78. if(options.msg !== '') {
  79. return options.msg;
  80. } else {
  81. return '';
  82. }
  83. }
  84. this.text = options.translation || "character word left max";
  85. this.text = this.text.split(' ');
  86. this.chars = "s ( )".split(' ');
  87. this.msg = null;
  88. switch(options.type) {
  89. case "char":
  90. if(options.count === defaults.count && options.text) {
  91. // x character(s) left
  92. this.msg = this.text[0] + this.chars[1] + this.chars[0] + this.chars[2] + " " + this.text[2];
  93. } else if(options.count === "up" && options.text) {
  94. // x characters (x max)
  95. this.msg = this.text[0] + this.chars[0] + " " + this.chars[1] + options.goal + " " + this.text[3] + this.chars[2];
  96. }
  97. break;
  98. case "word":
  99. if(options.count === defaults.count && options.text) {
  100. // x word(s) left
  101. this.msg = this.text[1] + this.chars[1] + this.chars[0] + this.chars[2] + " " + this.text[2];
  102. } else if(options.count === "up" && options.text) {
  103. // x word(s) (x max)
  104. this.msg = this.text[1] + this.chars[1] + this.chars[0] + this.chars[2] + " " + this.chars[1] + options.goal + " " + this.text[3] + this.chars[2];
  105. }
  106. break;
  107. default:
  108. }
  109. return this.msg;
  110. },
  111. /* Returns the amount of words passed in the val argument
  112. * @param val Words to count */
  113. getWords: function(val) {
  114. if(val !== "") {
  115. return $.trim(val).replace(/\s+/g, " ").split(" ").length;
  116. } else {
  117. return 0;
  118. }
  119. },
  120. updateCounter: function(e) {
  121. // Save reference to $(this)
  122. var $this = $(this);
  123. // Is the goal amount passed? (most common when pasting)
  124. if(countIndex < 0 || countIndex > options.goal) {
  125. methods.passedGoal($this);
  126. }
  127. var parent = $( $countObj ).parent();
  128. // Counting characters...
  129. if(options.type === defaults.type) {
  130. // ...down
  131. if(options.count === defaults.count) {
  132. countIndex = options.goal - $this.val().length;
  133. // Prevent negative counter
  134. if(countIndex <= 0) {
  135. $countObj.text('0');
  136. } else {
  137. $countObj.text(countIndex);
  138. }
  139. // ...up
  140. } else if(options.count === 'up') {
  141. countIndex = $this.val().length;
  142. $countObj.text(countIndex);
  143. }
  144. // Counting words...
  145. } else if(options.type === 'word') {
  146. // ...down
  147. if(options.count === defaults.count) {
  148. // Count words
  149. countIndex = methods.getWords($this.val());
  150. if(countIndex <= options.goal) {
  151. // Subtract
  152. countIndex = options.goal - countIndex;
  153. // Update text
  154. $countObj.text(countIndex);
  155. } else {
  156. // Don't show negative number count
  157. $countObj.text('0');
  158. }
  159. // ...up
  160. } else if(options.count === 'up') {
  161. countIndex = methods.getWords($this.val());
  162. $countObj.text(countIndex);
  163. }
  164. }
  165. var percent = ( countIndex / options.goal );
  166. if ( percent == 0 ) {
  167. jQuery( parent ).removeClass( 'near-limit' );
  168. jQuery( parent ).addClass( 'at-limit' );
  169. } else if ( percent <= .34 ) {
  170. jQuery( parent ).removeClass( 'at-limit' );
  171. jQuery( parent ).addClass( 'near-limit' );
  172. } else {
  173. jQuery( parent ).removeClass( 'near-limit' );
  174. jQuery( parent ).removeClass( 'at-limit' );
  175. }
  176. return;
  177. },
  178. /* Stops the ability to type */
  179. doStopTyping: function(e) {
  180. // backspace, delete, tab, left, up, right, down, end, home, spacebar
  181. var keys = [46, 8, 9, 35, 36, 37, 38, 39, 40, 32];
  182. if(methods.isGoalReached(e)) {
  183. // NOTE: // Using ( !$.inArray(e.keyCode, keys) ) as a condition causes delays
  184. if(e.keyCode !== keys[0] && e.keyCode !== keys[1] && e.keyCode !== keys[2] && e.keyCode !== keys[3] && e.keyCode !== keys[4] && e.keyCode !== keys[5] && e.keyCode !== keys[6] && e.keyCode !== keys[7] && e.keyCode !== keys[8]) {
  185. // Stop typing when counting characters
  186. if(options.type === defaults.type) {
  187. return false;
  188. // Counting words, only allow backspace & delete
  189. } else if(e.keyCode !== keys[9] && e.keyCode !== keys[1] && options.type != defaults.type) {
  190. return true;
  191. } else {
  192. return false;
  193. }
  194. }
  195. }
  196. },
  197. /* Checks to see if the goal number has been reached */
  198. isGoalReached: function(e, _goal) {
  199. if(noLimit) {
  200. return false;
  201. }
  202. // Counting down
  203. if(options.count === defaults.count) {
  204. _goal = 0;
  205. return(countIndex <= _goal) ? true : false;
  206. } else {
  207. // Counting up
  208. _goal = options.goal;
  209. return(countIndex >= _goal) ? true : false;
  210. }
  211. },
  212. /* Removes extra words when the amount of words in the input go over the desired goal.
  213. * @param {Number} numOfWords Amount of words you would like shown
  214. * @param {String} text The full text to condense */
  215. wordStrip: function(numOfWords, text) {
  216. var wordCount = text.replace(/\s+/g, ' ').split(' ').length;
  217. // Get the word count by counting the spaces (after eliminating trailing white space)
  218. text = $.trim(text);
  219. // Make it worth executing
  220. if(numOfWords <= 0 || numOfWords === wordCount) {
  221. return text;
  222. } else {
  223. text = $.trim(text).split(' ');
  224. text.splice(numOfWords, wordCount, '');
  225. return $.trim(text.join(' '));
  226. }
  227. },
  228. /* If the goal is passed, trim the chars/words down to what is allowed. Also, reset the counter. */
  229. passedGoal: function($obj) {
  230. var userInput = $obj.val();
  231. if(options.type === 'word') {
  232. $obj.val(methods.wordStrip(options.goal, userInput));
  233. }
  234. if(options.type === 'char') {
  235. $obj.val(userInput.substring(0, options.goal));
  236. }
  237. // Reset to 0
  238. if(options.type === 'down') {
  239. $countObj.val('0');
  240. }
  241. // Reset to goal
  242. if(options.type === 'up') {
  243. $countObj.val(options.goal);
  244. }
  245. }
  246. };
  247. return this.each(function() {
  248. methods.init($(this));
  249. });
  250. }
  251. });
  252. })(jQuery);