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