bootstrap-maxlength-1.js 16 KB

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