123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- (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: false, // if true the indicator it's always shown.
- threshold: 10, // Represents how many chars left are needed to show up the counter
- warningClass: 'label label-success',
- limitReachedClass: 'label label-important label-danger',
- separator: ' / ',
- preText: '',
- postText: '',
- showMaxLength: true,
- placement: 'bottom',
- 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.
- allowOverMax: false // false = use maxlength attribute and browswer functionality.
- // true = removes maxlength attribute and replaces with 'data-bs-mxl'.
- // Form submit validation is handled on your own. when maxlength has been exceeded 'overmax' class added to element
- };
- if ($.isFunction(options) && !callback) {
- callback = options;
- options = {};
- }
- options = $.extend(defaults, options);
- /**
- * 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(new RegExp('\r?\n', 'g'), '\n');
- }
- var currentLength = 0;
- if (options.utf8) {
- currentLength = utf8Length(text);
- } else {
- currentLength = text.length;
- }
- return currentLength;
- }
- /**
- * Truncate the text of the specified input.
- *
- * @param input
- * @param limit
- */
- function truncateChars(input, maxlength) {
- var text = input.val();
- var newlines = 0;
- if (options.twoCharLinebreak) {
- text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
- if (text.substr(text.length - 1) === '\n' && text.length % 2 === 1) {
- newlines = 1;
- }
- }
- input.val(text.substr(0, maxlength - newlines));
- }
- /**
- * Return the length of the specified input in UTF8 encoding.
- *
- * @param input
- * @return {number}
- */
- function utf8Length(string) {
- var utf8length = 0;
- for (var n = 0; n < string.length; n++) {
- var c = string.charCodeAt(n);
- if (c < 128) {
- utf8length++;
- }
- else if ((c > 127) && (c < 2048)) {
- utf8length = utf8length + 2;
- }
- else {
- utf8length = utf8length + 3;
- }
- }
- return utf8length;
- }
- /**
- * Return true if the indicator should be showing up.
- *
- * @param input
- * @param thereshold
- * @param maxlength
- * @return {number}
- */
- function charsLeftThreshold(input, thereshold, maxlength) {
- var output = true;
- if (!options.alwaysShow && (maxlength - inputLength(input) > thereshold)) {
- 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) {
- 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.allowOverMax) {
- // 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 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 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 retrieves the maximum length of currentInput
- *
- * @param currentInput
- * @return {number}
- *
- */
- function getMaxLength(currentInput) {
- var attr = 'maxlength';
- if (options.allowOverMax) {
- attr = 'data-bs-mxl';
- }
- return currentInput.attr(attr) || currentInput.attr('size');
- }
- return this.each(function () {
- var currentInput = $(this),
- maxLengthCurrentInput,
- maxLengthIndicator;
- $(window).resize(function () {
- if (maxLengthIndicator) {
- place(currentInput, maxLengthIndicator);
- }
- });
- if (options.allowOverMax) {
- $(this).attr('data-bs-mxl', $(this).attr('maxlength'));
- $(this).removeAttr('maxlength');
- }
- function firstInit() {
- var maxlengthContent = updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, '0');
- maxLengthCurrentInput = getMaxLength(currentInput);
- if (!maxLengthIndicator) {
- maxLengthIndicator = $('<span class="bootstrap-maxlength"></span>').css({
- display: 'none',
- position: 'absolute',
- whiteSpace: 'nowrap',
- zIndex: 1099
- }).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);
- }
- //reposition the indicator if placement "bottom-right-inside" & "top-right-inside" is used
- if (options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside') {
- place(currentInput, maxLengthIndicator);
- }
- return output;
- });
- });
- }
- });
- }(jQuery));
|