Merge "Add support for 'hu-formal'"
[lhc/web/wiklou.git] / resources / src / jquery / jquery.lengthLimit.js
1 /**
2 * @class jQuery.plugin.lengthLimit
3 */
4 ( function ( $, mw ) {
5
6 var
7 eventKeys = [
8 'keyup.lengthLimit',
9 'keydown.lengthLimit',
10 'change.lengthLimit',
11 'mouseup.lengthLimit',
12 'cut.lengthLimit',
13 'paste.lengthLimit',
14 'focus.lengthLimit',
15 'blur.lengthLimit'
16 ].join( ' ' ),
17 trimByteLength = require( 'mediawiki.String' ).trimByteLength,
18 trimCodePointLength = require( 'mediawiki.String' ).trimCodePointLength;
19
20 /**
21 * Utility function to trim down a string, based on byteLimit
22 * and given a safe start position. It supports insertion anywhere
23 * in the string, so "foo" to "fobaro" if limit is 4 will result in
24 * "fobo", not "foba". Basically emulating the native maxlength by
25 * reconstructing where the insertion occurred.
26 *
27 * @method trimByteLength
28 * @deprecated Use `require( 'mediawiki.String' ).trimByteLength` instead.
29 * @static
30 * @param {string} safeVal Known value that was previously returned by this
31 * function, if none, pass empty string.
32 * @param {string} newVal New value that may have to be trimmed down.
33 * @param {number} byteLimit Number of bytes the value may be in size.
34 * @param {Function} [filterFn] See jQuery#byteLimit.
35 * @return {Object}
36 * @return {string} return.newVal
37 * @return {boolean} return.trimmed
38 */
39 mw.log.deprecate( $, 'trimByteLength', trimByteLength,
40 'Use require( \'mediawiki.String\' ).trimByteLength instead.', '$.trimByteLength' );
41
42 function lengthLimit( trimFn, limit, filterFn ) {
43 var allowNativeMaxlength = trimFn === trimByteLength;
44
45 // If the first argument is the function,
46 // set filterFn to the first argument's value and ignore the second argument.
47 if ( $.isFunction( limit ) ) {
48 filterFn = limit;
49 limit = undefined;
50 // Either way, verify it is a function so we don't have to call
51 // isFunction again after this.
52 } else if ( !filterFn || !$.isFunction( filterFn ) ) {
53 filterFn = undefined;
54 }
55
56 // The following is specific to each element in the collection.
57 return this.each( function ( i, el ) {
58 var $el, elLimit, prevSafeVal;
59
60 $el = $( el );
61
62 // If no limit was passed to lengthLimit(), use the maxlength value.
63 // Can't re-use 'limit' variable because it's in the higher scope
64 // that would affect the next each() iteration as well.
65 // Note that we use attribute to read the value instead of property,
66 // because in Chrome the maxLength property by default returns the
67 // highest supported value (no indication that it is being enforced
68 // by choice). We don't want to bind all of this for some ridiculously
69 // high default number, unless it was explicitly set in the HTML.
70 // Also cast to a (primitive) number (most commonly because the maxlength
71 // attribute contains a string, but theoretically the limit parameter
72 // could be something else as well).
73 elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit );
74
75 // If there is no (valid) limit passed or found in the property,
76 // skip this. The < 0 check is required for Firefox, which returns
77 // -1 (instead of undefined) for maxLength if it is not set.
78 if ( !elLimit || elLimit < 0 ) {
79 return;
80 }
81
82 if ( filterFn ) {
83 // Save function for reference
84 $el.data( 'lengthLimit.callback', filterFn );
85 }
86
87 // Remove old event handlers (if there are any)
88 $el.off( '.lengthLimit' );
89
90 if ( filterFn || !allowNativeMaxlength ) {
91 // Disable the native maxLength (if there is any), because it interferes
92 // with the (differently calculated) character/byte limit.
93 // Aside from being differently calculated,
94 // we also support a callback which can make it to allow longer
95 // values (e.g. count "Foo" from "User:Foo").
96 // maxLength is a strange property. Removing or setting the property to
97 // undefined directly doesn't work. Instead, it can only be unset internally
98 // by the browser when removing the associated attribute (Firefox/Chrome).
99 // https://bugs.chromium.org/p/chromium/issues/detail?id=136004
100 $el.removeAttr( 'maxlength' );
101
102 } else {
103 // For $.byteLimit only, if we don't have a callback,
104 // the byteLimit can only be lower than the native maxLength limit
105 // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce
106 // the native limit for efficiency when possible (it will make the while-loop below
107 // faster by there being less left to interate over). This does not work for $.codePointLimit
108 // (code units for surrogates represent half a character each).
109 $el.attr( 'maxlength', elLimit );
110 }
111
112 // Safe base value, used to determine the path between the previous state
113 // and the state that triggered the event handler below - and enforce the
114 // limit approppiately (e.g. don't chop from the end if text was inserted
115 // at the beginning of the string).
116 prevSafeVal = '';
117
118 // We need to listen to after the change has already happened because we've
119 // learned that trying to guess the new value and canceling the event
120 // accordingly doesn't work because the new value is not always as simple as:
121 // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag
122 // replacements, and custom input methods and what not.
123 // Even though we only trim input after it was changed (never prevent it), we do
124 // listen on events that input text, because there are cases where the text has
125 // changed while text is being entered and keyup/change will not be fired yet
126 // (such as holding down a single key, fires keydown, and after each keydown,
127 // we can trim the previous one).
128 // See https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for
129 // the order and characteristics of the key events.
130 $el.on( eventKeys, function () {
131 var res = trimFn(
132 prevSafeVal,
133 this.value,
134 elLimit,
135 filterFn
136 );
137
138 // Only set value property if it was trimmed, because whenever the
139 // value property is set, the browser needs to re-initiate the text context,
140 // which moves the cursor at the end the input, moving it away from wherever it was.
141 // This is a side-effect of limiting after the fact.
142 if ( res.trimmed === true ) {
143 this.value = res.newVal;
144 // Trigger a 'change' event to let other scripts attached to this node know that the value
145 // was changed. This will also call ourselves again, but that's okay, it'll be a no-op.
146 $el.trigger( 'change' );
147 }
148 // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
149 // trimFn to compare the new value to an empty string instead of the
150 // old value, resulting in trimming always from the end (T42850).
151 prevSafeVal = res.newVal;
152 } );
153 } );
154 }
155
156 /**
157 * Enforces a byte limit on an input field, assuming UTF-8 encoding, for situations
158 * when, for example, a database field has a byte limit rather than a character limit.
159 * Plugin rationale: Browser has native maxlength for number of characters (technically,
160 * UTF-16 code units), this plugin exists to limit number of bytes instead.
161 *
162 * Can be called with a custom limit (to use that limit instead of the maxlength attribute
163 * value), a filter function (in case the limit should apply to something other than the
164 * exact input value), or both. Order of parameters is important!
165 *
166 * @param {number} [limit] Limit to enforce, fallsback to maxLength-attribute,
167 * called with fetched value as argument.
168 * @param {Function} [filterFn] Function to call on the string before assessing the length.
169 * @return {jQuery}
170 * @chainable
171 */
172 $.fn.byteLimit = function ( limit, filterFn ) {
173 return lengthLimit.call( this, trimByteLength, limit, filterFn );
174 };
175
176 /**
177 * Enforces a codepoint (character) limit on an input field.
178 *
179 * For unfortunate historical reasons, browsers' native maxlength counts [the number of UTF-16
180 * code units rather than Unicode codepoints] [1], which means that codepoints outside the Basic
181 * Multilingual Plane (e.g. many emojis) count as 2 characters each. This plugin exists to
182 * correct this.
183 *
184 * [1]: https://www.w3.org/TR/html5/sec-forms.html#limiting-user-input-length-the-maxlength-attribute
185 *
186 * Can be called with a custom limit (to use that limit instead of the maxlength attribute
187 * value), a filter function (in case the limit should apply to something other than the
188 * exact input value), or both. Order of parameters is important!
189 *
190 * @param {number} [limit] Limit to enforce, fallsback to maxLength-attribute,
191 * called with fetched value as argument.
192 * @param {Function} [filterFn] Function to call on the string before assessing the length.
193 * @return {jQuery}
194 * @chainable
195 */
196 $.fn.codePointLimit = function ( limit, filterFn ) {
197 return lengthLimit.call( this, trimCodePointLength, limit, filterFn );
198 };
199
200 /**
201 * @class jQuery
202 * @mixins jQuery.plugin.lengthLimit
203 */
204 }( jQuery, mediaWiki ) );