diff options
Diffstat (limited to 'resources/jquery/jquery.byteLimit.js')
-rw-r--r-- | resources/jquery/jquery.byteLimit.js | 266 |
1 files changed, 204 insertions, 62 deletions
diff --git a/resources/jquery/jquery.byteLimit.js b/resources/jquery/jquery.byteLimit.js index 10411924..75dc2b90 100644 --- a/resources/jquery/jquery.byteLimit.js +++ b/resources/jquery/jquery.byteLimit.js @@ -1,87 +1,229 @@ /** - * jQuery byteLimit + * jQuery byteLimit plugin. * - * @author Jan Paul Posma + * @author Jan Paul Posma, 2011 + * @author Timo Tijhof, 2011-2012 */ -( function( $ ) { +( function ( $ ) { /** - * Enforces a byte limit to a textbox, so that UTF-8 entries are counted as well, when, for example, - * a databae field has a byte limit rather than a character limit. - * Plugin rationale: Browser has native maxlength for number of characters, this plugin exists to - * limit number of bytes instead. + * Utility function to trim down a string, based on byteLimit + * and given a safe start position. It supports insertion anywhere + * in the string, so "foo" to "fobaro" if limit is 4 will result in + * "fobo", not "foba". Basically emulating the native maxlength by + * reconstructing where the insertion occured. * - * Can be called with a custom limit (to use that limit instead of the maxlength attribute value), - * a filter function (in case the limit should apply to something other than the exact input value), - * or both. Order of arguments is important! + * @param {string} safeVal Known value that was previously returned by this + * function, if none, pass empty string. + * @param {string} newVal New value that may have to be trimmed down. + * @param {number} byteLimit Number of bytes the value may be in size. + * @param {Function} fn [optional] See $.fn.byteLimit. + * @return {Object} Object with: + * - {string} newVal + * - {boolean} trimmed + */ + function trimValForByteLength( safeVal, newVal, byteLimit, fn ) { + var startMatches, endMatches, matchesLen, inpParts, + oldVal = safeVal; + + // Run the hook if one was provided, but only on the length + // assessment. The value itself is not to be affected by the hook. + if ( $.byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) { + // Limit was not reached, just remember the new value + // and let the user continue. + return { + newVal: newVal, + trimmed: false + }; + } + + // Current input is longer than the active limit. + // Figure out what was added and limit the addition. + startMatches = 0; + endMatches = 0; + + // It is important that we keep the search within the range of + // the shortest string's length. + // Imagine a user adds text that matches the end of the old value + // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without + // limiting both searches to the shortest length, endMatches would + // also be 3. + matchesLen = Math.min( newVal.length, oldVal.length ); + + // Count same characters from the left, first. + // (if "foo" -> "foofoo", assume addition was at the end). + while ( + startMatches < matchesLen && + oldVal.charAt( startMatches ) === newVal.charAt( startMatches ) + ) { + startMatches += 1; + } + + while ( + endMatches < ( matchesLen - startMatches ) && + oldVal.charAt( oldVal.length - 1 - endMatches ) === newVal.charAt( newVal.length - 1 - endMatches ) + ) { + endMatches += 1; + } + + inpParts = [ + // Same start + newVal.substring( 0, startMatches ), + // Inserted content + newVal.substring( startMatches, newVal.length - endMatches ), + // Same end + newVal.substring( newVal.length - endMatches ) + ]; + + // Chop off characters from the end of the "inserted content" string + // until the limit is statisfied. + if ( fn ) { + while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit ) { + inpParts[1] = inpParts[1].slice( 0, -1 ); + } + } else { + while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) { + inpParts[1] = inpParts[1].slice( 0, -1 ); + } + } + + newVal = inpParts.join( '' ); + + return { + newVal: newVal, + trimmed: true + }; + } + + var eventKeys = [ + 'keyup.byteLimit', + 'keydown.byteLimit', + 'change.byteLimit', + 'mouseup.byteLimit', + 'cut.byteLimit', + 'paste.byteLimit', + 'focus.byteLimit', + 'blur.byteLimit' + ].join( ' ' ); + + /** + * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well, + * when, for example, a database field has a byte limit rather than a character limit. + * Plugin rationale: Browser has native maxlength for number of characters, this plugin + * exists to limit number of bytes instead. + * + * Can be called with a custom limit (to use that limit instead of the maxlength attribute + * value), a filter function (in case the limit should apply to something other than the + * exact input value), or both. Order of parameters is important! * * @context {jQuery} Instance of jQuery for one or more input elements - * @param limit {Number} (optional) Limit to enforce, fallsback to maxLength-attribute, - * called with fetched value as argument. - * @param fn {Function} (optional) Function to call on the input string before assessing the length + * @param {Number} limit [optional] Limit to enforce, fallsback to maxLength-attribute, + * called with fetched value as argument. + * @param {Function} fn [optional] Function to call on the string before assessing the length. * @return {jQuery} The context */ - $.fn.byteLimit = function( limit, fn ) { + $.fn.byteLimit = function ( limit, fn ) { // If the first argument is the function, // set fn to the first argument's value and ignore the second argument. if ( $.isFunction( limit ) ) { fn = limit; limit = undefined; + // Either way, verify it is a function so we don't have to call + // isFunction again after this. + } else if ( !fn || !$.isFunction( fn ) ) { + fn = undefined; } - // Default limit to current attribute value - if ( limit === undefined ) { - limit = this.prop( 'maxLength' ); - } + // The following is specific to each element in the collection. + return this.each( function ( i, el ) { + var $el, elLimit, prevSafeVal; - // Update/set attribute value, but only if there is no callback set. - // If there's a callback set, it's possible that the limit being enforced - // is too low (ie. if the callback would return "Foo" for "User:Foo"). - // Usually this isn't a problem since browsers ignore maxLength when setting - // the value property through JavaScript, but Safari 4 violates that rule, so - // we have to remove or not set the property if we have a callback. - if ( fn == undefined ) { - this.prop( 'maxLength', limit ); - } else { - this.removeProp( 'maxLength' ); - } + $el = $( el ); - // Nothing passed and/or empty attribute, return without binding an event. - if ( limit === undefined ) { - return this; - } + // If no limit was passed to byteLimit(), use the maxlength value. + // Can't re-use 'limit' variable because it's in the higher scope + // that would affect the next each() iteration as well. + // Note that we use attribute to read the value instead of property, + // because in Chrome the maxLength property by default returns the + // highest supported value (no indication that it is being enforced + // by choice). We don't want to bind all of this for some ridiculously + // high default number, unless it was explicitly set in the HTML. + // Also cast to a (primitive) number (most commonly because the maxlength + // attribute contains a string, but theoretically the limit parameter + // could be something else as well). + elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit ); - // Save function for reference - this.data( 'byteLimit-callback', fn ); - - // We've got something, go for it: - return this.keypress( function( e ) { - // First check to see if this is actually a character key - // being pressed. - // Based on key-event info from http://unixpapa.com/js/key.html - // jQuery should also normalize e.which to be consistent cross-browser, - // however the same check is still needed regardless of jQuery. - - // Note: At the moment, for some older opera versions (~< 10.5) - // some special keys won't be recognized (aka left arrow key). - // Backspace will be, so not big issue. - - if ( e.which === 0 || e.charCode === 0 || e.which === 8 || - e.ctrlKey || e.altKey || e.metaKey ) - { - return true; //a special key (backspace, etc) so don't interfere. + // If there is no (valid) limit passed or found in the property, + // skip this. The < 0 check is required for Firefox, which returns + // -1 (instead of undefined) for maxLength if it is not set. + if ( !elLimit || elLimit < 0 ) { + return; } - var val = fn !== undefined ? fn( $( this ).val() ): $( this ).val(), - len = $.byteLength( val ), - // Note that keypress returns a character code point, not a keycode. - // However, this may not be super reliable depending on how keys come in... - charLen = $.byteLength( String.fromCharCode( e.which ) ); + if ( fn ) { + // Save function for reference + $el.data( 'byteLimit.callback', fn ); + } + + // Remove old event handlers (if there are any) + $el.off( '.byteLimit' ); - if ( ( len + charLen ) > limit ) { - e.preventDefault(); + if ( fn ) { + // Disable the native maxLength (if there is any), because it interferes + // with the (differently calculated) byte limit. + // Aside from being differently calculated (average chars with byteLimit + // is lower), we also support a callback which can make it to allow longer + // values (e.g. count "Foo" from "User:Foo"). + // maxLength is a strange property. Removing or setting the property to + // undefined directly doesn't work. Instead, it can only be unset internally + // by the browser when removing the associated attribute (Firefox/Chrome). + // http://code.google.com/p/chromium/issues/detail?id=136004 + $el.removeAttr( 'maxlength' ); + + } else { + // If we don't have a callback the bytelimit can only be lower than the charlimit + // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce + // the native limit for efficiency when possible (it will make the while-loop below + // faster by there being less left to interate over). + $el.attr( 'maxlength', elLimit ); } - }); - }; -} )( jQuery ); + + // Safe base value, used to determine the path between the previous state + // and the state that triggered the event handler below - and enforce the + // limit approppiately (e.g. don't chop from the end if text was inserted + // at the beginning of the string). + prevSafeVal = ''; + + // We need to listen to after the change has already happened because we've + // learned that trying to guess the new value and canceling the event + // accordingly doesn't work because the new value is not always as simple as: + // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag + // replacements, and custom input methods and what not. + // Even though we only trim input after it was changed (never prevent it), we do + // listen on events that input text, because there are cases where the text has + // changed while text is being entered and keyup/change will not be fired yet + // (such as holding down a single key, fires keydown, and after each keydown, + // we can trim the previous one). + // See http://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for + // the order and characteristics of the key events. + $el.on( eventKeys, function () { + var res = trimValForByteLength( + prevSafeVal, + this.value, + elLimit, + fn + ); + + // Only set value property if it was trimmed, because whenever the + // value property is set, the browser needs to re-initiate the text context, + // which moves the cursor at the end the input, moving it away from wherever it was. + // This is a side-effect of limiting after the fact. + if ( res.trimmed === true ) { + this.value = res.newVal; + prevSafeVal = res.newVal; + } + } ); + } ); + }; +}( jQuery ) ); |