Merge "Correctly use $wgFeedLimit in page history feed"
[lhc/web/wiklou.git] / resources / jquery / jquery.byteLimit.js
1 /**
2 * jQuery byteLimit plugin.
3 *
4 * @author Jan Paul Posma, 2011
5 * @author Timo Tijhof, 2011-2012
6 */
7 ( function ( $ ) {
8
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;
28
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 }
39
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;
44
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 );
52
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 }
61
62 while (
63 endMatches < ( matchesLen - startMatches ) &&
64 oldVal.charAt( oldVal.length - 1 - endMatches ) === newVal.charAt( newVal.length - 1 - endMatches )
65 ) {
66 endMatches += 1;
67 }
68
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 ];
77
78 // Chop off characters from the end of the "inserted content" string
79 // until the limit is statisfied.
80 if ( fn ) {
81 // stop, when there is nothing to slice - bug 41450
82 while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[1].length > 0 ) {
83 inpParts[1] = inpParts[1].slice( 0, -1 );
84 }
85 } else {
86 while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) {
87 inpParts[1] = inpParts[1].slice( 0, -1 );
88 }
89 }
90
91 newVal = inpParts.join( '' );
92
93 return {
94 newVal: newVal,
95 trimmed: true
96 };
97 }
98
99 var eventKeys = [
100 'keyup.byteLimit',
101 'keydown.byteLimit',
102 'change.byteLimit',
103 'mouseup.byteLimit',
104 'cut.byteLimit',
105 'paste.byteLimit',
106 'focus.byteLimit',
107 'blur.byteLimit'
108 ].join( ' ' );
109
110 /**
111 * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well,
112 * when, for example, a database field has a byte limit rather than a character limit.
113 * Plugin rationale: Browser has native maxlength for number of characters, this plugin
114 * exists to limit number of bytes instead.
115 *
116 * Can be called with a custom limit (to use that limit instead of the maxlength attribute
117 * value), a filter function (in case the limit should apply to something other than the
118 * exact input value), or both. Order of parameters is important!
119 *
120 * @context {jQuery} Instance of jQuery for one or more input elements
121 * @param {Number} limit [optional] Limit to enforce, fallsback to maxLength-attribute,
122 * called with fetched value as argument.
123 * @param {Function} fn [optional] Function to call on the string before assessing the length.
124 * @return {jQuery} The context
125 */
126 $.fn.byteLimit = function ( limit, fn ) {
127 // If the first argument is the function,
128 // set fn to the first argument's value and ignore the second argument.
129 if ( $.isFunction( limit ) ) {
130 fn = limit;
131 limit = undefined;
132 // Either way, verify it is a function so we don't have to call
133 // isFunction again after this.
134 } else if ( !fn || !$.isFunction( fn ) ) {
135 fn = undefined;
136 }
137
138 // The following is specific to each element in the collection.
139 return this.each( function ( i, el ) {
140 var $el, elLimit, prevSafeVal;
141
142 $el = $( el );
143
144 // If no limit was passed to byteLimit(), use the maxlength value.
145 // Can't re-use 'limit' variable because it's in the higher scope
146 // that would affect the next each() iteration as well.
147 // Note that we use attribute to read the value instead of property,
148 // because in Chrome the maxLength property by default returns the
149 // highest supported value (no indication that it is being enforced
150 // by choice). We don't want to bind all of this for some ridiculously
151 // high default number, unless it was explicitly set in the HTML.
152 // Also cast to a (primitive) number (most commonly because the maxlength
153 // attribute contains a string, but theoretically the limit parameter
154 // could be something else as well).
155 elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit );
156
157 // If there is no (valid) limit passed or found in the property,
158 // skip this. The < 0 check is required for Firefox, which returns
159 // -1 (instead of undefined) for maxLength if it is not set.
160 if ( !elLimit || elLimit < 0 ) {
161 return;
162 }
163
164 if ( fn ) {
165 // Save function for reference
166 $el.data( 'byteLimit.callback', fn );
167 }
168
169 // Remove old event handlers (if there are any)
170 $el.off( '.byteLimit' );
171
172 if ( fn ) {
173 // Disable the native maxLength (if there is any), because it interferes
174 // with the (differently calculated) byte limit.
175 // Aside from being differently calculated (average chars with byteLimit
176 // is lower), we also support a callback which can make it to allow longer
177 // values (e.g. count "Foo" from "User:Foo").
178 // maxLength is a strange property. Removing or setting the property to
179 // undefined directly doesn't work. Instead, it can only be unset internally
180 // by the browser when removing the associated attribute (Firefox/Chrome).
181 // http://code.google.com/p/chromium/issues/detail?id=136004
182 $el.removeAttr( 'maxlength' );
183
184 } else {
185 // If we don't have a callback the bytelimit can only be lower than the charlimit
186 // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce
187 // the native limit for efficiency when possible (it will make the while-loop below
188 // faster by there being less left to interate over).
189 $el.attr( 'maxlength', elLimit );
190 }
191
192
193 // Safe base value, used to determine the path between the previous state
194 // and the state that triggered the event handler below - and enforce the
195 // limit approppiately (e.g. don't chop from the end if text was inserted
196 // at the beginning of the string).
197 prevSafeVal = '';
198
199 // We need to listen to after the change has already happened because we've
200 // learned that trying to guess the new value and canceling the event
201 // accordingly doesn't work because the new value is not always as simple as:
202 // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag
203 // replacements, and custom input methods and what not.
204 // Even though we only trim input after it was changed (never prevent it), we do
205 // listen on events that input text, because there are cases where the text has
206 // changed while text is being entered and keyup/change will not be fired yet
207 // (such as holding down a single key, fires keydown, and after each keydown,
208 // we can trim the previous one).
209 // See http://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for
210 // the order and characteristics of the key events.
211 $el.on( eventKeys, function () {
212 var res = trimValForByteLength(
213 prevSafeVal,
214 this.value,
215 elLimit,
216 fn
217 );
218
219 // Only set value property if it was trimmed, because whenever the
220 // value property is set, the browser needs to re-initiate the text context,
221 // which moves the cursor at the end the input, moving it away from wherever it was.
222 // This is a side-effect of limiting after the fact.
223 if ( res.trimmed === true ) {
224 this.value = res.newVal;
225 }
226 // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
227 // trimValForByteLength to compare the new value to an empty string instead of the
228 // old value, resulting in trimming always from the end (bug 40850).
229 prevSafeVal = res.newVal;
230 } );
231 } );
232 };
233 }( jQuery ) );