Merge "Minor bugfix to IP::prettifyIP."
[lhc/web/wiklou.git] / resources / jquery / jquery.byteLimit.js
index 484651e..a8c0b06 100644 (file)
@@ -1,5 +1,5 @@
 /**
- * jQuery byteLimit plugin
+ * jQuery byteLimit plugin.
  *
  * @author Jan Paul Posma, 2011
  * @author Timo Tijhof, 2011-2012
 ( function ( $ ) {
 
        /**
-        * Enforces a byte limit to a textbox, 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.
+        * 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 ) {
+                       // stop, when there is nothing to slice - bug 41450
+                       while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[1].length > 0 ) {
+                               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 ) {
                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;
                }
 
-               // The following is specific to each element in the collection
+               // The following is specific to each element in the collection.
                return this.each( function ( i, el ) {
-                       var $el, elLimit;
+                       var $el, elLimit, prevSafeVal;
 
                        $el = $( el );
 
-                       // Default limit to current attribute value
+                       // 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 affects the next each() iteration as well.
-                       elLimit = limit === undefined ? $el.prop( 'maxLength' ) : limit;
-       
+                       // 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 );
+
                        // 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.
                                return;
                        }
 
-                       // 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 ) {
-                               $el.prop( 'maxLength', elLimit );
+                       if ( fn ) {
+                               // Save function for reference
+                               $el.data( 'byteLimit.callback', fn );
+                       }
+
+                       // Remove old event handlers (if there are any)
+                       $el.off( '.byteLimit' );
+
+                       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 {
-                               $el.removeProp( 'maxLength' );
+                               // 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 );
                        }
-       
-                       // Save function for reference
-                       $el.data( 'byteLimitCallback', fn );
-       
-                       // We've got something, go for it:
-                       $el.keypress( function ( e ) {
-                               var val, len, charLen;
-                               // 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 )
-                               {
-                                       // A special key (backspace, etc) so don't interfere
-                                       return true;
-                               }
-       
-                               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 ( ( len + charLen ) > elLimit ) {
-                                       e.preventDefault();
+
+
+                       // 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;
                                }
-                       });
-               });
+                               // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
+                               // trimValForByteLength to compare the new value to an empty string instead of the
+                               // old value, resulting in trimming always from the end (bug 40850).
+                               prevSafeVal = res.newVal;
+                       } );
+               } );
        };
 }( jQuery ) );