/* ========================================================== * bootstrap-maxlength.js v1.9.0 * * Copyright (c) 2013-2020 Maurizio Napoleoni; * * Licensed under the terms of the MIT license. * See: https://github.com/mimo84/bootstrap-maxlength/blob/master/LICENSE * ========================================================== */ /*global jQuery*/ (function ($) { 'use strict'; /** * We need an event when the elements are destroyed * because if an input is removed, we have to remove the * maxlength object associated (if any). * From: * http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom */ if (!$.event.special.destroyed) { $.event.special.destroyed = { remove: function (o) { if (o.handler) { o.handler(); } } }; } $.fn.extend({ maxlength: function (options, callback) { var documentBody = $('body'), defaults = { showOnReady: false, // true to always show when indicator is ready alwaysShow: true, // if true the indicator it's always shown. threshold: 0, // Represents how many chars left are needed to show up the counter warningClass: 'small form-text text-muted', limitReachedClass: 'small form-text text-danger', separator: ' / ', preText: '', postText: '', showMaxLength: true, placement: 'bottom-right-inside', message: null, // an alternative way to provide the message text showCharsTyped: true, // show the number of characters typed and not the number of characters remaining validate: false, // if the browser doesn't support the maxlength attribute, attempt to type more than the indicated chars, will be prevented. utf8: false, // counts using bytesize rather than length. eg: '£' is counted as 2 characters. appendToParent: false, // append the indicator to the input field's parent instead of body twoCharLinebreak: true, // count linebreak as 2 characters to match IE/Chrome textarea validation. As well as DB storage. customMaxAttribute: null, // null = use maxlength attribute and browser functionality, string = use specified attribute instead. allowOverMax: false, // Form submit validation is handled on your own. when maxlength has been exceeded 'overmax' class added to element zIndex: 1099 }; if ($.isFunction(options) && !callback) { callback = options; options = {}; } options = $.extend(defaults, options); /** * Return the byte count of the specified character in UTF8 encoding. * Note: This won't cover UTF-8 characters that are 4 bytes long. * * @param input * @return {number} */ function utf8CharByteCount(character) { var c = character.charCodeAt(); // Not c then 0, else c < 128 then 1, else c < 2048 then 2, else 3 return !c ? 0 : c < 128 ? 1 : c < 2048 ? 2 : 3; } /** * Return the length of the specified input in UTF8 encoding. * * @param input * @return {number} */ function utf8Length(string) { return string.split("") .map(utf8CharByteCount) // Prevent reduce from throwing an error if the string is empty. .concat(0) .reduce(function (sum, val) { return sum + val; }); } /** * Return the length of the specified input. * * @param input * @return {number} */ function inputLength(input) { var text = input.val(); if (options.twoCharLinebreak) { // Count all line breaks as 2 characters text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n'); } else { // Remove all double-character (\r\n) linebreaks, so they're counted only once. text = text.replace(/(?:\r\n|\r|\n)/g, '\n'); } var currentLength = 0; if (options.utf8) { currentLength = utf8Length(text); } else { currentLength = text.length; } // Remove "C:\fakepath\" from counter when using file input // Fix https://github.com/mimo84/bootstrap-maxlength/issues/146 if (input.prop("type") === "file" && input.val() !== "") { currentLength -= 12; } return currentLength; } /** * Truncate the text of the specified input. * * @param input * @param limit */ function truncateChars(input, maxlength) { var text = input.val(); if (options.twoCharLinebreak) { text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n'); if (text[text.length - 1] === '\n') { maxlength -= text.length % 2; } } if (options.utf8) { var indexedSize = text.split("").map(utf8CharByteCount); for ( var removedBytes = 0, bytesPastMax = utf8Length(text) - maxlength; removedBytes < bytesPastMax; removedBytes += indexedSize.pop() ); maxlength -= (maxlength - indexedSize.length); } input.val(text.substr(0, maxlength)); } /** * Return true if the indicator should be showing up. * * @param input * @param threshold * @param maxlength * @return {number} */ function charsLeftThreshold(input, threshold, maxlength) { var output = true; if (!options.alwaysShow && (maxlength - inputLength(input) > threshold)) { output = false; } return output; } /** * Returns how many chars are left to complete the fill up of the form. * * @param input * @param maxlength * @return {number} */ function remainingChars(input, maxlength) { var length = maxlength - inputLength(input); return length; } /** * When called displays the indicator. * * @param indicator */ function showRemaining(currentInput, indicator) { indicator.css({ display: 'block' }); currentInput.trigger('maxlength.shown'); } /** * When called shows the indicator. * * @param indicator */ function hideRemaining(currentInput, indicator) { if (options.alwaysShow) { return; } indicator.css({ display: 'none' }); currentInput.trigger('maxlength.hidden'); } /** * This function updates the value in the indicator * * @param maxLengthThisInput * @param typedChars * @return String */ function updateMaxLengthHTML(currentInputText, maxLengthThisInput, typedChars) { var output = ''; if (options.message) { if (typeof options.message === 'function') { output = options.message(currentInputText, maxLengthThisInput); } else { output = options.message.replace('%charsTyped%', typedChars) .replace('%charsRemaining%', maxLengthThisInput - typedChars) .replace('%charsTotal%', maxLengthThisInput); } } else { if (options.preText) { output += options.preText; } if (!options.showCharsTyped) { output += maxLengthThisInput - typedChars; } else { output += typedChars; } if (options.showMaxLength) { output += options.separator + maxLengthThisInput; } if (options.postText) { output += options.postText; } } return output; } /** * This function updates the value of the counter in the indicator. * Wants as parameters: the number of remaining chars, the element currently managed, * the maxLength for the current input and the indicator generated for it. * * @param remaining * @param currentInput * @param maxLengthCurrentInput * @param maxLengthIndicator */ function manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator) { if (maxLengthIndicator) { maxLengthIndicator.html(updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, (maxLengthCurrentInput - remaining))); if (remaining > 0) { if (charsLeftThreshold(currentInput, options.threshold, maxLengthCurrentInput)) { showRemaining(currentInput, maxLengthIndicator.removeClass(options.limitReachedClass).addClass(options.warningClass)); } else { hideRemaining(currentInput, maxLengthIndicator); } } else { showRemaining(currentInput, maxLengthIndicator.removeClass(options.warningClass).addClass(options.limitReachedClass)); } } if (options.customMaxAttribute) { // class to use for form validation on custom maxlength attribute if (remaining < 0) { currentInput.addClass('overmax'); } else { currentInput.removeClass('overmax'); } } } /** * This function returns an object containing all the * informations about the position of the current input * * @param currentInput * @return object {bottom height left right top width} * */ function getPosition(currentInput) { var el = currentInput[0]; return $.extend({}, (typeof el.getBoundingClientRect === 'function') ? el.getBoundingClientRect() : { width: el.offsetWidth, height: el.offsetHeight }, currentInput.offset()); } /** * This function places the maxLengthIndicator based on placement config object. * * @param {object} placement * @param {$} maxLengthIndicator * @return null * */ function placeWithCSS(placement, maxLengthIndicator) { if (!placement || !maxLengthIndicator) { return; } var POSITION_KEYS = [ 'top', 'bottom', 'left', 'right', 'position' ]; var cssPos = {}; // filter css properties to position $.each(POSITION_KEYS, function (i, key) { var val = options.placement[key]; if (typeof val !== 'undefined') { cssPos[key] = val; } }); maxLengthIndicator.css(cssPos); return; } /** * This function places the maxLengthIndicator at the * top / bottom / left / right of the currentInput * * @param currentInput * @param maxLengthIndicator * @return null * */ function place(currentInput, maxLengthIndicator) { var pos = getPosition(currentInput); // Supports custom placement handler if ($.type(options.placement) === 'function') { options.placement(currentInput, maxLengthIndicator, pos); return; } // Supports custom placement via css positional properties if ($.isPlainObject(options.placement)) { placeWithCSS(options.placement, maxLengthIndicator); return; } var inputOuter = currentInput.outerWidth(), outerWidth = maxLengthIndicator.outerWidth(), actualWidth = maxLengthIndicator.width(), actualHeight = maxLengthIndicator.height(); // get the right position if the indicator is appended to the input's parent if (options.appendToParent) { pos.top -= currentInput.parent().offset().top; pos.left -= currentInput.parent().offset().left; } switch (options.placement) { case 'bottom': maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 }); break; case 'top': maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 }); break; case 'left': maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth }); break; case 'right': maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }); break; case 'bottom-right': maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width }); break; case 'top-right': maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter }); break; case 'top-left': maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left - outerWidth }); break; case 'bottom-left': maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left - outerWidth }); break; case 'centered-right': maxLengthIndicator.css({ top: pos.top + (actualHeight / 2), left: pos.left + inputOuter - outerWidth - 3 }); break; // Some more options for placements case 'bottom-right-inside': maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width - outerWidth }); break; case 'top-right-inside': maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter - outerWidth }); break; case 'top-left-inside': maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left }); break; case 'bottom-left-inside': maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left }); break; } } /** * This function returns true if the indicator position needs to * be recalculated when the currentInput changes * * @return {boolean} * */ function isPlacementMutable() { return options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside' || typeof options.placement === 'function' || (options.message && typeof options.message === 'function'); } /** * This function retrieves the maximum length of currentInput * * @param currentInput * @return {number} * */ function getMaxLength(currentInput) { var max = currentInput.attr('maxlength') || options.customMaxAttribute; if (options.customMaxAttribute && !options.allowOverMax) { var custom = currentInput.attr(options.customMaxAttribute); if (!max || custom < max) { max = custom; } } if (!max) { max = currentInput.attr('size'); } return max; } return this.each(function () { var currentInput = $(this), maxLengthCurrentInput, maxLengthIndicator; $(window).resize(function () { if (maxLengthIndicator) { place(currentInput, maxLengthIndicator); } }); function firstInit() { var maxlengthContent = updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, '0'); maxLengthCurrentInput = getMaxLength(currentInput); if (!maxLengthIndicator) { maxLengthIndicator = $('').css({ display: 'none', position: 'absolute', whiteSpace: 'nowrap', zIndex: options.zIndex }).html(maxlengthContent); } // We need to detect resizes if we are dealing with a textarea: if (currentInput.is('textarea')) { currentInput.data('maxlenghtsizex', currentInput.outerWidth()); currentInput.data('maxlenghtsizey', currentInput.outerHeight()); currentInput.mouseup(function () { if (currentInput.outerWidth() !== currentInput.data('maxlenghtsizex') || currentInput.outerHeight() !== currentInput.data('maxlenghtsizey')) { place(currentInput, maxLengthIndicator); } currentInput.data('maxlenghtsizex', currentInput.outerWidth()); currentInput.data('maxlenghtsizey', currentInput.outerHeight()); }); } if (options.appendToParent) { currentInput.parent().append(maxLengthIndicator); currentInput.parent().css('position', 'relative'); } else { documentBody.append(maxLengthIndicator); } var remaining = remainingChars(currentInput, getMaxLength(currentInput)); manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator); place(currentInput, maxLengthIndicator); } if (options.showOnReady) { currentInput.ready(function () { firstInit(); }); } else { currentInput.focus(function () { firstInit(); }); } currentInput.on('maxlength.reposition', function () { place(currentInput, maxLengthIndicator); }); currentInput.on('destroyed', function () { if (maxLengthIndicator) { maxLengthIndicator.remove(); } }); currentInput.on('blur', function () { if (maxLengthIndicator && !options.showOnReady) { maxLengthIndicator.remove(); } }); currentInput.on('input', function () { var maxlength = getMaxLength(currentInput), remaining = remainingChars(currentInput, maxlength), output = true; if (options.validate && remaining < 0) { truncateChars(currentInput, maxlength); output = false; } else { manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator); } if (isPlacementMutable()) { place(currentInput, maxLengthIndicator); } return output; }); }); } }); }(jQuery));