bootstrap-maxlength.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. (function ($) {
  2. 'use strict';
  3. /**
  4. * We need an event when the elements are destroyed
  5. * because if an input is removed, we have to remove the
  6. * maxlength object associated (if any).
  7. * From:
  8. * http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
  9. */
  10. if (!$.event.special.destroyed) {
  11. $.event.special.destroyed = {
  12. remove: function (o) {
  13. if (o.handler) {
  14. o.handler();
  15. }
  16. }
  17. };
  18. }
  19. $.fn.extend({
  20. maxlength: function (options, callback) {
  21. var documentBody = $('body'),
  22. defaults = {
  23. showOnReady: false, // true to always show when indicator is ready
  24. alwaysShow: false, // if true the indicator it's always shown.
  25. threshold: 10, // Represents how many chars left are needed to show up the counter
  26. warningClass: 'label label-success',
  27. limitReachedClass: 'label label-important label-danger',
  28. separator: ' / ',
  29. preText: '',
  30. postText: '',
  31. showMaxLength: true,
  32. placement: 'bottom',
  33. message: null, // an alternative way to provide the message text
  34. showCharsTyped: true, // show the number of characters typed and not the number of characters remaining
  35. validate: false, // if the browser doesn't support the maxlength attribute, attempt to type more than
  36. // the indicated chars, will be prevented.
  37. utf8: false, // counts using bytesize rather than length. eg: '£' is counted as 2 characters.
  38. appendToParent: false, // append the indicator to the input field's parent instead of body
  39. twoCharLinebreak: true, // count linebreak as 2 characters to match IE/Chrome textarea validation. As well as DB storage.
  40. customMaxAttribute: null, // null = use maxlength attribute and browser functionality, string = use specified attribute instead.
  41. allowOverMax: false
  42. // Form submit validation is handled on your own. when maxlength has been exceeded 'overmax' class added to element
  43. };
  44. if ($.isFunction(options) && !callback) {
  45. callback = options;
  46. options = {};
  47. }
  48. options = $.extend(defaults, options);
  49. /**
  50. * Return the byte count of the specified character in UTF8 encoding.
  51. * Note: This won't cover UTF-8 characters that are 4 bytes long.
  52. *
  53. * @param input
  54. * @return {number}
  55. */
  56. function utf8CharByteCount(character) {
  57. var c = character.charCodeAt();
  58. // Not c then 0, else c < 128 then 1, else c < 2048 then 2, else 3
  59. return !c ? 0 : c < 128 ? 1 : c < 2048 ? 2 : 3;
  60. }
  61. /**
  62. * Return the length of the specified input in UTF8 encoding.
  63. *
  64. * @param input
  65. * @return {number}
  66. */
  67. function utf8Length(string) {
  68. return string.split("")
  69. .map(utf8CharByteCount)
  70. // Prevent reduce from throwing an error if the string is empty.
  71. .concat(0)
  72. .reduce(function(sum, val) { return sum + val; });
  73. }
  74. /**
  75. * Return the length of the specified input.
  76. *
  77. * @param input
  78. * @return {number}
  79. */
  80. function inputLength(input) {
  81. var text = input.val();
  82. if (options.twoCharLinebreak) {
  83. // Count all line breaks as 2 characters
  84. text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
  85. } else {
  86. // Remove all double-character (\r\n) linebreaks, so they're counted only once.
  87. text = text.replace(new RegExp('\r?\n', 'g'), '\n');
  88. }
  89. var currentLength = 0;
  90. if (options.utf8) {
  91. currentLength = utf8Length(text);
  92. } else {
  93. currentLength = text.length;
  94. }
  95. return currentLength;
  96. }
  97. /**
  98. * Truncate the text of the specified input.
  99. *
  100. * @param input
  101. * @param limit
  102. */
  103. function truncateChars(input, maxlength) {
  104. var text = input.val();
  105. if (options.twoCharLinebreak) {
  106. text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
  107. if (text[text.length - 1] === '\n') {
  108. maxlength -= text.length % 2;
  109. }
  110. }
  111. if (options.utf8) {
  112. var indexedSize = text.split("").map(utf8CharByteCount);
  113. for (
  114. var removedBytes = 0,
  115. bytesPastMax = utf8Length(text) - maxlength
  116. ;removedBytes < bytesPastMax
  117. ;removedBytes += indexedSize.pop()
  118. );
  119. maxlength -= (maxlength - indexedSize.length);
  120. }
  121. input.val(text.substr(0, maxlength));
  122. }
  123. /**
  124. * Return true if the indicator should be showing up.
  125. *
  126. * @param input
  127. * @param threshold
  128. * @param maxlength
  129. * @return {number}
  130. */
  131. function charsLeftThreshold(input, threshold, maxlength) {
  132. var output = true;
  133. if (!options.alwaysShow && (maxlength - inputLength(input) > threshold)) {
  134. output = false;
  135. }
  136. return output;
  137. }
  138. /**
  139. * Returns how many chars are left to complete the fill up of the form.
  140. *
  141. * @param input
  142. * @param maxlength
  143. * @return {number}
  144. */
  145. function remainingChars(input, maxlength) {
  146. var length = maxlength - inputLength(input);
  147. return length;
  148. }
  149. /**
  150. * When called displays the indicator.
  151. *
  152. * @param indicator
  153. */
  154. function showRemaining(currentInput, indicator) {
  155. indicator.css({
  156. display: 'block'
  157. });
  158. currentInput.trigger('maxlength.shown');
  159. }
  160. /**
  161. * When called shows the indicator.
  162. *
  163. * @param indicator
  164. */
  165. function hideRemaining(currentInput, indicator) {
  166. if (options.alwaysShow) {
  167. return;
  168. }
  169. indicator.css({
  170. display: 'none'
  171. });
  172. currentInput.trigger('maxlength.hidden');
  173. }
  174. /**
  175. * This function updates the value in the indicator
  176. *
  177. * @param maxLengthThisInput
  178. * @param typedChars
  179. * @return String
  180. */
  181. function updateMaxLengthHTML(currentInputText, maxLengthThisInput, typedChars) {
  182. var output = '';
  183. if (options.message) {
  184. if (typeof options.message === 'function') {
  185. output = options.message(currentInputText, maxLengthThisInput);
  186. } else {
  187. output = options.message.replace('%charsTyped%', typedChars)
  188. .replace('%charsRemaining%', maxLengthThisInput - typedChars)
  189. .replace('%charsTotal%', maxLengthThisInput);
  190. }
  191. } else {
  192. if (options.preText) {
  193. output += options.preText;
  194. }
  195. if (!options.showCharsTyped) {
  196. output += maxLengthThisInput - typedChars;
  197. }
  198. else {
  199. output += typedChars;
  200. }
  201. if (options.showMaxLength) {
  202. output += options.separator + maxLengthThisInput;
  203. }
  204. if (options.postText) {
  205. output += options.postText;
  206. }
  207. }
  208. return output;
  209. }
  210. /**
  211. * This function updates the value of the counter in the indicator.
  212. * Wants as parameters: the number of remaining chars, the element currently managed,
  213. * the maxLength for the current input and the indicator generated for it.
  214. *
  215. * @param remaining
  216. * @param currentInput
  217. * @param maxLengthCurrentInput
  218. * @param maxLengthIndicator
  219. */
  220. function manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator) {
  221. if (maxLengthIndicator) {
  222. maxLengthIndicator.html(updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, (maxLengthCurrentInput - remaining)));
  223. if (remaining > 0) {
  224. if (charsLeftThreshold(currentInput, options.threshold, maxLengthCurrentInput)) {
  225. showRemaining(currentInput, maxLengthIndicator.removeClass(options.limitReachedClass).addClass(options.warningClass));
  226. } else {
  227. hideRemaining(currentInput, maxLengthIndicator);
  228. }
  229. } else {
  230. showRemaining(currentInput, maxLengthIndicator.removeClass(options.warningClass).addClass(options.limitReachedClass));
  231. }
  232. }
  233. if (options.customMaxAttribute) {
  234. // class to use for form validation on custom maxlength attribute
  235. if (remaining < 0) {
  236. currentInput.addClass('overmax');
  237. } else {
  238. currentInput.removeClass('overmax');
  239. }
  240. }
  241. }
  242. /**
  243. * This function returns an object containing all the
  244. * informations about the position of the current input
  245. *
  246. * @param currentInput
  247. * @return object {bottom height left right top width}
  248. *
  249. */
  250. function getPosition(currentInput) {
  251. var el = currentInput[0];
  252. return $.extend({}, (typeof el.getBoundingClientRect === 'function') ? el.getBoundingClientRect() : {
  253. width: el.offsetWidth,
  254. height: el.offsetHeight
  255. }, currentInput.offset());
  256. }
  257. /**
  258. * This function places the maxLengthIndicator based on placement config object.
  259. *
  260. * @param {object} placement
  261. * @param {$} maxLengthIndicator
  262. * @return null
  263. *
  264. */
  265. function placeWithCSS(placement, maxLengthIndicator) {
  266. if (!placement || !maxLengthIndicator){
  267. return;
  268. }
  269. var POSITION_KEYS = [
  270. 'top',
  271. 'bottom',
  272. 'left',
  273. 'right',
  274. 'position'
  275. ];
  276. var cssPos = {};
  277. // filter css properties to position
  278. $.each(POSITION_KEYS, function (i, key) {
  279. var val = options.placement[key];
  280. if (typeof val !== 'undefined'){
  281. cssPos[key] = val;
  282. }
  283. });
  284. maxLengthIndicator.css(cssPos);
  285. return;
  286. }
  287. /**
  288. * This function places the maxLengthIndicator at the
  289. * top / bottom / left / right of the currentInput
  290. *
  291. * @param currentInput
  292. * @param maxLengthIndicator
  293. * @return null
  294. *
  295. */
  296. function place(currentInput, maxLengthIndicator) {
  297. var pos = getPosition(currentInput);
  298. // Supports custom placement handler
  299. if ($.type(options.placement) === 'function'){
  300. options.placement(currentInput, maxLengthIndicator, pos);
  301. return;
  302. }
  303. // Supports custom placement via css positional properties
  304. if ($.isPlainObject(options.placement)){
  305. placeWithCSS(options.placement, maxLengthIndicator);
  306. return;
  307. }
  308. var inputOuter = currentInput.outerWidth(),
  309. outerWidth = maxLengthIndicator.outerWidth(),
  310. actualWidth = maxLengthIndicator.width(),
  311. actualHeight = maxLengthIndicator.height();
  312. // get the right position if the indicator is appended to the input's parent
  313. if (options.appendToParent) {
  314. pos.top -= currentInput.parent().offset().top;
  315. pos.left -= currentInput.parent().offset().left;
  316. }
  317. switch (options.placement) {
  318. case 'bottom':
  319. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 });
  320. break;
  321. case 'top':
  322. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 });
  323. break;
  324. case 'left':
  325. maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth });
  326. break;
  327. case 'right':
  328. maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width });
  329. break;
  330. case 'bottom-right':
  331. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width });
  332. break;
  333. case 'top-right':
  334. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter });
  335. break;
  336. case 'top-left':
  337. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left - outerWidth });
  338. break;
  339. case 'bottom-left':
  340. maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left - outerWidth });
  341. break;
  342. case 'centered-right':
  343. maxLengthIndicator.css({ top: pos.top + (actualHeight / 2), left: pos.left + inputOuter - outerWidth - 3 });
  344. break;
  345. // Some more options for placements
  346. case 'bottom-right-inside':
  347. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width - outerWidth });
  348. break;
  349. case 'top-right-inside':
  350. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter - outerWidth });
  351. break;
  352. case 'top-left-inside':
  353. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left });
  354. break;
  355. case 'bottom-left-inside':
  356. maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left });
  357. break;
  358. }
  359. }
  360. /**
  361. * This function returns true if the indicator position needs to
  362. * be recalculated when the currentInput changes
  363. *
  364. * @return {boolean}
  365. *
  366. */
  367. function isPlacementMutable() {
  368. return options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside' || typeof options.placement === 'function' || (options.message && typeof options.message === 'function');
  369. }
  370. /**
  371. * This function retrieves the maximum length of currentInput
  372. *
  373. * @param currentInput
  374. * @return {number}
  375. *
  376. */
  377. function getMaxLength(currentInput) {
  378. var max = currentInput.attr('maxlength') || options.customMaxAttribute;
  379. if (options.customMaxAttribute && !options.allowOverMax) {
  380. var custom = currentInput.attr(options.customMaxAttribute);
  381. if (!max || custom < max) {
  382. max = custom;
  383. }
  384. }
  385. if (!max) {
  386. max = currentInput.attr('size');
  387. }
  388. return max;
  389. }
  390. return this.each(function () {
  391. var currentInput = $(this),
  392. maxLengthCurrentInput,
  393. maxLengthIndicator;
  394. $(window).resize(function () {
  395. if (maxLengthIndicator) {
  396. place(currentInput, maxLengthIndicator);
  397. }
  398. });
  399. function firstInit() {
  400. var maxlengthContent = updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, '0');
  401. maxLengthCurrentInput = getMaxLength(currentInput);
  402. if (!maxLengthIndicator) {
  403. maxLengthIndicator = $('<span class="bootstrap-maxlength"></span>').css({
  404. display: 'none',
  405. position: 'absolute',
  406. whiteSpace: 'nowrap',
  407. zIndex: 1099
  408. }).html(maxlengthContent);
  409. }
  410. // We need to detect resizes if we are dealing with a textarea:
  411. if (currentInput.is('textarea')) {
  412. currentInput.data('maxlenghtsizex', currentInput.outerWidth());
  413. currentInput.data('maxlenghtsizey', currentInput.outerHeight());
  414. currentInput.mouseup(function () {
  415. if (currentInput.outerWidth() !== currentInput.data('maxlenghtsizex') || currentInput.outerHeight() !== currentInput.data('maxlenghtsizey')) {
  416. place(currentInput, maxLengthIndicator);
  417. }
  418. currentInput.data('maxlenghtsizex', currentInput.outerWidth());
  419. currentInput.data('maxlenghtsizey', currentInput.outerHeight());
  420. });
  421. }
  422. if (options.appendToParent) {
  423. currentInput.parent().append(maxLengthIndicator);
  424. currentInput.parent().css('position', 'relative');
  425. } else {
  426. documentBody.append(maxLengthIndicator);
  427. }
  428. var remaining = remainingChars(currentInput, getMaxLength(currentInput));
  429. manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
  430. place(currentInput, maxLengthIndicator);
  431. }
  432. if (options.showOnReady) {
  433. currentInput.ready(function () {
  434. firstInit();
  435. });
  436. } else {
  437. currentInput.focus(function () {
  438. firstInit();
  439. });
  440. }
  441. currentInput.on('maxlength.reposition', function () {
  442. place(currentInput, maxLengthIndicator);
  443. });
  444. currentInput.on('destroyed', function () {
  445. if (maxLengthIndicator) {
  446. maxLengthIndicator.remove();
  447. }
  448. });
  449. currentInput.on('blur', function () {
  450. if (maxLengthIndicator && !options.showOnReady) {
  451. maxLengthIndicator.remove();
  452. }
  453. });
  454. currentInput.on('input', function () {
  455. var maxlength = getMaxLength(currentInput),
  456. remaining = remainingChars(currentInput, maxlength),
  457. output = true;
  458. if (options.validate && remaining < 0) {
  459. truncateChars(currentInput, maxlength);
  460. output = false;
  461. } else {
  462. manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
  463. }
  464. if (isPlacementMutable()) {
  465. place(currentInput, maxLengthIndicator);
  466. }
  467. return output;
  468. });
  469. });
  470. }
  471. });
  472. }(jQuery));