Merge "Add ss_active_users in SiteStats::isSane"
[mediawiki.git] / resources / jquery / jquery.byteLimit.js
blobf2b98f09bb73f7d0a24a86fd1a04a52b74b264f0
1 /**
2  * jQuery byteLimit plugin.
3  *
4  * @author Jan Paul Posma, 2011
5  * @author Timo Tijhof, 2011-2012
6  */
7 ( function ( $ ) {
9         /**
10          * Utility function to trim down a string, based on byteLimit
11          * and given a safe start position. It supports insertion anywhere
12          * in the string, so "foo" to "fobaro" if limit is 4 will result in
13          * "fobo", not "foba". Basically emulating the native maxlength by
14          * reconstructing where the insertion occured.
15          *
16          * @param {string} safeVal Known value that was previously returned by this
17          * function, if none, pass empty string.
18          * @param {string} newVal New value that may have to be trimmed down.
19          * @param {number} byteLimit Number of bytes the value may be in size.
20          * @param {Function} fn [optional] See $.fn.byteLimit.
21          * @return {Object} Object with:
22          *  - {string} newVal
23          *  - {boolean} trimmed
24          */
25         function trimValForByteLength( safeVal, newVal, byteLimit, fn ) {
26                 var startMatches, endMatches, matchesLen, inpParts,
27                         oldVal = safeVal;
29                 // Run the hook if one was provided, but only on the length
30                 // assessment. The value itself is not to be affected by the hook.
31                 if ( $.byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) {
32                         // Limit was not reached, just remember the new value
33                         // and let the user continue.
34                         return {
35                                 newVal: newVal,
36                                 trimmed: false
37                         };
38                 }
40                 // Current input is longer than the active limit.
41                 // Figure out what was added and limit the addition.
42                 startMatches = 0;
43                 endMatches = 0;
45                 // It is important that we keep the search within the range of
46                 // the shortest string's length.
47                 // Imagine a user adds text that matches the end of the old value
48                 // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without
49                 // limiting both searches to the shortest length, endMatches would
50                 // also be 3.
51                 matchesLen = Math.min( newVal.length, oldVal.length );
53                 // Count same characters from the left, first.
54                 // (if "foo" -> "foofoo", assume addition was at the end).
55                 while (
56                         startMatches < matchesLen &&
57                         oldVal.charAt( startMatches ) === newVal.charAt( startMatches )
58                 ) {
59                         startMatches += 1;
60                 }
62                 while (
63                         endMatches < ( matchesLen - startMatches ) &&
64                         oldVal.charAt( oldVal.length - 1 - endMatches ) === newVal.charAt( newVal.length - 1 - endMatches )
65                 ) {
66                         endMatches += 1;
67                 }
69                 inpParts = [
70                         // Same start
71                         newVal.substring( 0, startMatches ),
72                         // Inserted content
73                         newVal.substring( startMatches, newVal.length - endMatches ),
74                         // Same end
75                         newVal.substring( newVal.length - endMatches )
76                 ];
78                 // Chop off characters from the end of the "inserted content" string
79                 // until the limit is statisfied.
80                 if ( fn ) {
81                         while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit ) {
82                                 inpParts[1] = inpParts[1].slice( 0, -1 );
83                         }
84                 } else {
85                         while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) {
86                                 inpParts[1] = inpParts[1].slice( 0, -1 );
87                         }
88                 }
90                 newVal = inpParts.join( '' );
92                 return {
93                         newVal: newVal,
94                         trimmed: true
95                 };
96         }
98         var eventKeys = [
99                 'keyup.byteLimit',
100                 'keydown.byteLimit',
101                 'change.byteLimit',
102                 'mouseup.byteLimit',
103                 'cut.byteLimit',
104                 'paste.byteLimit',
105                 'focus.byteLimit',
106                 'blur.byteLimit'
107         ].join( ' ' );
109         /**
110          * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well,
111          * when, for example, a database field has a byte limit rather than a character limit.
112          * Plugin rationale: Browser has native maxlength for number of characters, this plugin
113          * exists to limit number of bytes instead.
114          *
115          * Can be called with a custom limit (to use that limit instead of the maxlength attribute
116          * value), a filter function (in case the limit should apply to something other than the
117          * exact input value), or both. Order of parameters is important!
118          *
119          * @context {jQuery} Instance of jQuery for one or more input elements
120          * @param {Number} limit [optional] Limit to enforce, fallsback to maxLength-attribute,
121          *  called with fetched value as argument.
122          * @param {Function} fn [optional] Function to call on the string before assessing the length.
123          * @return {jQuery} The context
124          */
125         $.fn.byteLimit = function ( limit, fn ) {
126                 // If the first argument is the function,
127                 // set fn to the first argument's value and ignore the second argument.
128                 if ( $.isFunction( limit ) ) {
129                         fn = limit;
130                         limit = undefined;
131                 // Either way, verify it is a function so we don't have to call
132                 // isFunction again after this.
133                 } else if ( !fn || !$.isFunction( fn ) ) {
134                         fn = undefined;
135                 }
137                 // The following is specific to each element in the collection.
138                 return this.each( function ( i, el ) {
139                         var $el, elLimit, prevSafeVal;
141                         $el = $( el );
143                         // If no limit was passed to byteLimit(), use the maxlength value.
144                         // Can't re-use 'limit' variable because it's in the higher scope
145                         // that would affect the next each() iteration as well.
146                         // Note that we use attribute to read the value instead of property,
147                         // because in Chrome the maxLength property by default returns the
148                         // highest supported value (no indication that it is being enforced
149                         // by choice). We don't want to bind all of this for some ridiculously
150                         // high default number, unless it was explicitly set in the HTML.
151                         // Also cast to a (primitive) number (most commonly because the maxlength
152                         // attribute contains a string, but theoretically the limit parameter
153                         // could be something else as well).
154                         elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit );
156                         // If there is no (valid) limit passed or found in the property,
157                         // skip this. The < 0 check is required for Firefox, which returns
158                         // -1  (instead of undefined) for maxLength if it is not set.
159                         if ( !elLimit || elLimit < 0 ) {
160                                 return;
161                         }
163                         if ( fn ) {
164                                 // Save function for reference
165                                 $el.data( 'byteLimit.callback', fn );
166                         }
168                         // Remove old event handlers (if there are any)
169                         $el.off( '.byteLimit' );
171                         if ( fn ) {
172                                 // Disable the native maxLength (if there is any), because it interferes
173                                 // with the (differently calculated) byte limit.
174                                 // Aside from being differently calculated (average chars with byteLimit
175                                 // is lower), we also support a callback which can make it to allow longer
176                                 // values (e.g. count "Foo" from "User:Foo").
177                                 // maxLength is a strange property. Removing or setting the property to
178                                 // undefined directly doesn't work. Instead, it can only be unset internally
179                                 // by the browser when removing the associated attribute (Firefox/Chrome).
180                                 // http://code.google.com/p/chromium/issues/detail?id=136004
181                                 $el.removeAttr( 'maxlength' );
183                         } else {
184                                 // If we don't have a callback the bytelimit can only be lower than the charlimit
185                                 // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce
186                                 // the native limit for efficiency when possible (it will make the while-loop below
187                                 // faster by there being less left to interate over).
188                                 $el.attr( 'maxlength', elLimit );
189                         }
192                         // Safe base value, used to determine the path between the previous state
193                         // and the state that triggered the event handler below - and enforce the
194                         // limit approppiately (e.g. don't chop from the end if text was inserted
195                         // at the beginning of the string).
196                         prevSafeVal = '';
198                         // We need to listen to after the change has already happened because we've
199                         // learned that trying to guess the new value and canceling the event
200                         // accordingly doesn't work because the new value is not always as simple as:
201                         // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag
202                         // replacements, and custom input methods and what not.
203                         // Even though we only trim input after it was changed (never prevent it), we do
204                         // listen on events that input text, because there are cases where the text has
205                         // changed while text is being entered and keyup/change will not be fired yet
206                         // (such as holding down a single key, fires keydown, and after each keydown,
207                         // we can trim the previous one).
208                         // See http://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for
209                         // the order and characteristics of the key events.
210                         $el.on( eventKeys, function () {
211                                 var res = trimValForByteLength(
212                                         prevSafeVal,
213                                         this.value,
214                                         elLimit,
215                                         fn
216                                 );
218                                 // Only set value property if it was trimmed, because whenever the
219                                 // value property is set, the browser needs to re-initiate the text context,
220                                 // which moves the cursor at the end the input, moving it away from wherever it was.
221                                 // This is a side-effect of limiting after the fact.
222                                 if ( res.trimmed === true ) {
223                                         this.value = res.newVal;
224                                 }
225                                 // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
226                                 // trimValForByteLength to compare the new value to an empty string instead of the
227                                 // old value, resulting in trimming always from the end (bug 40850).
228                                 prevSafeVal = res.newVal;
229                         } );
230                 } );
231         };
232 }( jQuery ) );