]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - resources/src/jquery/jquery.byteLimit.js
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / resources / src / jquery / jquery.byteLimit.js
1 /**
2  * @class jQuery.plugin.byteLimit
3  */
4 ( function ( $ ) {
5
6         var eventKeys = [
7                 'keyup.byteLimit',
8                 'keydown.byteLimit',
9                 'change.byteLimit',
10                 'mouseup.byteLimit',
11                 'cut.byteLimit',
12                 'paste.byteLimit',
13                 'focus.byteLimit',
14                 'blur.byteLimit'
15         ].join( ' ' );
16
17         /**
18          * Utility function to trim down a string, based on byteLimit
19          * and given a safe start position. It supports insertion anywhere
20          * in the string, so "foo" to "fobaro" if limit is 4 will result in
21          * "fobo", not "foba". Basically emulating the native maxlength by
22          * reconstructing where the insertion occurred.
23          *
24          * @static
25          * @param {string} safeVal Known value that was previously returned by this
26          * function, if none, pass empty string.
27          * @param {string} newVal New value that may have to be trimmed down.
28          * @param {number} byteLimit Number of bytes the value may be in size.
29          * @param {Function} [fn] See jQuery#byteLimit.
30          * @return {Object}
31          * @return {string} return.newVal
32          * @return {boolean} return.trimmed
33          */
34         $.trimByteLength = function ( safeVal, newVal, byteLimit, fn ) {
35                 var startMatches, endMatches, matchesLen, inpParts,
36                         oldVal = safeVal;
37
38                 // Run the hook if one was provided, but only on the length
39                 // assessment. The value itself is not to be affected by the hook.
40                 if ( $.byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) {
41                         // Limit was not reached, just remember the new value
42                         // and let the user continue.
43                         return {
44                                 newVal: newVal,
45                                 trimmed: false
46                         };
47                 }
48
49                 // Current input is longer than the active limit.
50                 // Figure out what was added and limit the addition.
51                 startMatches = 0;
52                 endMatches = 0;
53
54                 // It is important that we keep the search within the range of
55                 // the shortest string's length.
56                 // Imagine a user adds text that matches the end of the old value
57                 // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without
58                 // limiting both searches to the shortest length, endMatches would
59                 // also be 3.
60                 matchesLen = Math.min( newVal.length, oldVal.length );
61
62                 // Count same characters from the left, first.
63                 // (if "foo" -> "foofoo", assume addition was at the end).
64                 while (
65                         startMatches < matchesLen &&
66                         oldVal.charAt( startMatches ) === newVal.charAt( startMatches )
67                 ) {
68                         startMatches += 1;
69                 }
70
71                 while (
72                         endMatches < ( matchesLen - startMatches ) &&
73                         oldVal.charAt( oldVal.length - 1 - endMatches ) === newVal.charAt( newVal.length - 1 - endMatches )
74                 ) {
75                         endMatches += 1;
76                 }
77
78                 inpParts = [
79                         // Same start
80                         newVal.slice( 0, startMatches ),
81                         // Inserted content
82                         newVal.slice( startMatches, newVal.length - endMatches ),
83                         // Same end
84                         newVal.slice( newVal.length - endMatches )
85                 ];
86
87                 // Chop off characters from the end of the "inserted content" string
88                 // until the limit is statisfied.
89                 if ( fn ) {
90                         // stop, when there is nothing to slice - T43450
91                         while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[ 1 ].length > 0 ) {
92                                 inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -1 );
93                         }
94                 } else {
95                         while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) {
96                                 inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -1 );
97                         }
98                 }
99
100                 return {
101                         newVal: inpParts.join( '' ),
102                         // For pathological fn() that always returns a value longer than the limit, we might have
103                         // ended up not trimming - check for this case to avoid infinite loops
104                         trimmed: newVal !== inpParts.join( '' )
105                 };
106         };
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                                 // https://bugs.chromium.org/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 https://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                                         // Trigger a 'change' event to let other scripts attached to this node know that the value
223                                         // was changed. This will also call ourselves again, but that's okay, it'll be a no-op.
224                                         $el.trigger( 'change' );
225                                 }
226                                 // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
227                                 // trimByteLength to compare the new value to an empty string instead of the
228                                 // old value, resulting in trimming always from the end (T42850).
229                                 prevSafeVal = res.newVal;
230                         } );
231                 } );
232         };
233
234         /**
235          * @class jQuery
236          * @mixins jQuery.plugin.byteLimit
237          */
238 }( jQuery ) );