bootstrap-maxlength.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. (function ($) {
  2. 'use strict';
  3. /**
  4. * We need an event when the elements are destroyed
  5. * because if an input is remvoed, 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. showCharsTyped: true, // show the number of characters typed and not the number of characters remaining
  34. validate: false, // if the browser doesn't support the maxlength attribute, attempt to type more than
  35. // the indicated chars, will be prevented.
  36. utf8: false, // counts using bytesize rather than length. eg: '£' is counted as 2 characters.
  37. appendToParent: false, // append the indicator to the input field's parent instead of body
  38. twoCharLinebreak: true, // count linebreak as 2 characters to match IE/Chrome textarea validation. As well as DB storage.
  39. allowOverMax: false // false = use maxlength attribute and browswer functionality.
  40. // true = removes maxlength attribute and replaces with 'data-bs-mxl'.
  41. // Form submit validation is handled on your own. when maxlength has been exceeded 'overmax' class added to element
  42. };
  43. if ($.isFunction(options) && !callback) {
  44. callback = options;
  45. options = {};
  46. }
  47. options = $.extend(defaults, options);
  48. /**
  49. * Return the length of the specified input.
  50. *
  51. * @param input
  52. * @return {number}
  53. */
  54. function inputLength(input) {
  55. var text = input.val();
  56. if (options.twoCharLinebreak) {
  57. // Count all line breaks as 2 characters
  58. text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
  59. } else {
  60. // Remove all double-character (\r\n) linebreaks, so they're counted only once.
  61. text = text.replace(new RegExp('\r?\n', 'g'), '\n');
  62. }
  63. var currentLength = 0;
  64. if (options.utf8) {
  65. currentLength = utf8Length(text);
  66. } else {
  67. currentLength = text.length;
  68. }
  69. return currentLength;
  70. }
  71. /**
  72. * Truncate the text of the specified input.
  73. *
  74. * @param input
  75. * @param limit
  76. */
  77. function truncateChars(input, maxlength) {
  78. var text = input.val();
  79. var newlines = 0;
  80. if (options.twoCharLinebreak) {
  81. text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
  82. if (text.substr(text.length - 1) === '\n' && text.length % 2 === 1) {
  83. newlines = 1;
  84. }
  85. }
  86. input.val(text.substr(0, maxlength - newlines));
  87. }
  88. /**
  89. * Return the length of the specified input in UTF8 encoding.
  90. *
  91. * @param input
  92. * @return {number}
  93. */
  94. function utf8Length(string) {
  95. var utf8length = 0;
  96. for (var n = 0; n < string.length; n++) {
  97. var c = string.charCodeAt(n);
  98. if (c < 128) {
  99. utf8length++;
  100. }
  101. else if ((c > 127) && (c < 2048)) {
  102. utf8length = utf8length + 2;
  103. }
  104. else {
  105. utf8length = utf8length + 3;
  106. }
  107. }
  108. return utf8length;
  109. }
  110. /**
  111. * Return true if the indicator should be showing up.
  112. *
  113. * @param input
  114. * @param thereshold
  115. * @param maxlength
  116. * @return {number}
  117. */
  118. function charsLeftThreshold(input, thereshold, maxlength) {
  119. var output = true;
  120. if (!options.alwaysShow && (maxlength - inputLength(input) > thereshold)) {
  121. output = false;
  122. }
  123. return output;
  124. }
  125. /**
  126. * Returns how many chars are left to complete the fill up of the form.
  127. *
  128. * @param input
  129. * @param maxlength
  130. * @return {number}
  131. */
  132. function remainingChars(input, maxlength) {
  133. var length = maxlength - inputLength(input);
  134. return length;
  135. }
  136. /**
  137. * When called displays the indicator.
  138. *
  139. * @param indicator
  140. */
  141. function showRemaining(indicator) {
  142. indicator.css({
  143. display: 'block'
  144. });
  145. }
  146. /**
  147. * When called shows the indicator.
  148. *
  149. * @param indicator
  150. */
  151. function hideRemaining(indicator) {
  152. indicator.css({
  153. display: 'none'
  154. });
  155. }
  156. /**
  157. * This function updates the value in the indicator
  158. *
  159. * @param maxLengthThisInput
  160. * @param typedChars
  161. * @return String
  162. */
  163. function updateMaxLengthHTML(maxLengthThisInput, typedChars) {
  164. var output = '';
  165. if (options.message) {
  166. output = options.message.replace('%charsTyped%', typedChars)
  167. .replace('%charsRemaining%', maxLengthThisInput - typedChars)
  168. .replace('%charsTotal%', maxLengthThisInput);
  169. } else {
  170. if (options.preText) {
  171. output += options.preText;
  172. }
  173. if (!options.showCharsTyped) {
  174. output += maxLengthThisInput - typedChars;
  175. }
  176. else {
  177. output += typedChars;
  178. }
  179. if (options.showMaxLength) {
  180. output += options.separator + maxLengthThisInput;
  181. }
  182. if (options.postText) {
  183. output += options.postText;
  184. }
  185. }
  186. return output;
  187. }
  188. /**
  189. * This function updates the value of the counter in the indicator.
  190. * Wants as parameters: the number of remaining chars, the element currently managed,
  191. * the maxLength for the current input and the indicator generated for it.
  192. *
  193. * @param remaining
  194. * @param currentInput
  195. * @param maxLengthCurrentInput
  196. * @param maxLengthIndicator
  197. */
  198. function manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator) {
  199. maxLengthIndicator.html(updateMaxLengthHTML(maxLengthCurrentInput, (maxLengthCurrentInput - remaining)));
  200. if (remaining > 0) {
  201. if (charsLeftThreshold(currentInput, options.threshold, maxLengthCurrentInput)) {
  202. showRemaining(maxLengthIndicator.removeClass(options.limitReachedClass).addClass(options.warningClass));
  203. } else {
  204. hideRemaining(maxLengthIndicator);
  205. }
  206. } else {
  207. showRemaining(maxLengthIndicator.removeClass(options.warningClass).addClass(options.limitReachedClass));
  208. }
  209. if (options.allowOverMax) {
  210. // class to use for form validation on custom maxlength attribute
  211. if (remaining < 0) {
  212. currentInput.addClass('overmax');
  213. } else {
  214. currentInput.removeClass('overmax');
  215. }
  216. }
  217. }
  218. /**
  219. * This function returns an object containing all the
  220. * informations about the position of the current input
  221. *
  222. * @param currentInput
  223. * @return object {bottom height left right top width}
  224. *
  225. */
  226. function getPosition(currentInput) {
  227. var el = currentInput[0];
  228. return $.extend({}, (typeof el.getBoundingClientRect === 'function') ? el.getBoundingClientRect() : {
  229. width: el.offsetWidth,
  230. height: el.offsetHeight
  231. }, currentInput.offset());
  232. }
  233. /**
  234. * This function places the maxLengthIndicator at the
  235. * top / bottom / left / right of the currentInput
  236. *
  237. * @param currentInput
  238. * @param maxLengthIndicator
  239. * @return null
  240. *
  241. */
  242. function place(currentInput, maxLengthIndicator) {
  243. var pos = getPosition(currentInput),
  244. inputOuter = currentInput.outerWidth(),
  245. outerWidth = maxLengthIndicator.outerWidth(),
  246. actualWidth = maxLengthIndicator.width(),
  247. actualHeight = maxLengthIndicator.height();
  248. // get the right position if the indicator is appended to the input's parent
  249. if (options.appendToParent) {
  250. pos.top -= currentInput.parent().offset().top;
  251. pos.left -= currentInput.parent().offset().left;
  252. }
  253. switch (options.placement) {
  254. case 'bottom':
  255. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 });
  256. break;
  257. case 'top':
  258. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 });
  259. break;
  260. case 'left':
  261. maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth });
  262. break;
  263. case 'right':
  264. maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width });
  265. break;
  266. case 'bottom-right':
  267. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width });
  268. break;
  269. case 'top-right':
  270. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter });
  271. break;
  272. case 'top-left':
  273. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left - outerWidth });
  274. break;
  275. case 'bottom-left':
  276. maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left - outerWidth });
  277. break;
  278. case 'centered-right':
  279. maxLengthIndicator.css({ top: pos.top + (actualHeight / 2), left: pos.left + inputOuter - outerWidth - 3 });
  280. break;
  281. // Some more options for placements
  282. case 'bottom-right-inside':
  283. maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width - outerWidth });
  284. break;
  285. case 'top-right-inside':
  286. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter - outerWidth });
  287. break;
  288. case 'top-left-inside':
  289. maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left });
  290. break;
  291. case 'bottom-left-inside':
  292. maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left });
  293. break;
  294. }
  295. }
  296. /**
  297. * This function retrieves the maximum length of currentInput
  298. *
  299. * @param currentInput
  300. * @return {number}
  301. *
  302. */
  303. function getMaxLength(currentInput) {
  304. var attr = 'maxlength';
  305. if (options.allowOverMax) {
  306. attr = 'data-bs-mxl';
  307. }
  308. return currentInput.attr(attr) || currentInput.attr('size');
  309. }
  310. return this.each(function () {
  311. var currentInput = $(this),
  312. maxLengthCurrentInput,
  313. maxLengthIndicator;
  314. $(window).resize(function () {
  315. if (maxLengthIndicator) {
  316. place(currentInput, maxLengthIndicator);
  317. }
  318. });
  319. if (options.allowOverMax) {
  320. $(this).attr('data-bs-mxl', $(this).attr('maxlength'));
  321. $(this).removeAttr('maxlength');
  322. }
  323. function firstInit() {
  324. var maxlengthContent = updateMaxLengthHTML(maxLengthCurrentInput, '0');
  325. maxLengthCurrentInput = getMaxLength(currentInput);
  326. if (!maxLengthIndicator) {
  327. maxLengthIndicator = $('<span class="bootstrap-maxlength"></span>').css({
  328. display: 'none',
  329. position: 'absolute',
  330. whiteSpace: 'nowrap',
  331. zIndex: 1099
  332. }).html(maxlengthContent);
  333. }
  334. // We need to detect resizes if we are dealing with a textarea:
  335. if (currentInput.is('textarea')) {
  336. currentInput.data('maxlenghtsizex', currentInput.outerWidth());
  337. currentInput.data('maxlenghtsizey', currentInput.outerHeight());
  338. currentInput.mouseup(function () {
  339. if (currentInput.outerWidth() !== currentInput.data('maxlenghtsizex') || currentInput.outerHeight() !== currentInput.data('maxlenghtsizey')) {
  340. place(currentInput, maxLengthIndicator);
  341. }
  342. currentInput.data('maxlenghtsizex', currentInput.outerWidth());
  343. currentInput.data('maxlenghtsizey', currentInput.outerHeight());
  344. });
  345. }
  346. if (options.appendToParent) {
  347. currentInput.parent().append(maxLengthIndicator);
  348. currentInput.parent().css('position', 'relative');
  349. } else {
  350. documentBody.append(maxLengthIndicator);
  351. }
  352. var remaining = remainingChars(currentInput, getMaxLength(currentInput));
  353. manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
  354. place(currentInput, maxLengthIndicator);
  355. }
  356. if (options.showOnReady) {
  357. currentInput.ready(function () {
  358. firstInit();
  359. });
  360. } else {
  361. currentInput.focus(function () {
  362. firstInit();
  363. });
  364. }
  365. currentInput.on('destroyed', function () {
  366. if (maxLengthIndicator) {
  367. maxLengthIndicator.remove();
  368. }
  369. });
  370. currentInput.on('blur', function () {
  371. if (maxLengthIndicator && !options.showOnReady) {
  372. maxLengthIndicator.remove();
  373. }
  374. });
  375. currentInput.on('input', function () {
  376. var maxlength = getMaxLength(currentInput),
  377. remaining = remainingChars(currentInput, maxlength),
  378. output = true;
  379. if (options.validate && remaining < 0) {
  380. truncateChars(currentInput, maxlength);
  381. output = false;
  382. } else {
  383. manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
  384. }
  385. //reposition the indicator if placement "bottom-right-inside" & "top-right-inside" is used
  386. if (options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside') {
  387. place(currentInput, maxLengthIndicator);
  388. }
  389. return output;
  390. });
  391. });
  392. }
  393. });
  394. }(jQuery));