From: Bartosz Dziewoński Date: Thu, 8 Feb 2018 21:22:34 +0000 (+0100) Subject: Rename jquery.byteLimit→lengthLimit, mediawiki.widgets.visibleByteLimit→visibleLength... X-Git-Tag: 1.31.0-rc.0~523 X-Git-Url: https://git.cyclocoop.org/%242?a=commitdiff_plain;h=777336288bc807a4e8ec1a391c00ff470f7849b5;p=lhc%2Fweb%2Fwiklou.git Rename jquery.byteLimit→lengthLimit, mediawiki.widgets.visibleByteLimit→visibleLengthLimit In change Ia1269fd898dabbcf1582618eab46cef97e10a3b1 I want to add functions that deal with codepoints instead of bytes to these modules, after which the names wouldn't make sense. Doing this in a separate commit to make the diffs clearer. Change-Id: Ia554eb2265248e72b04fce69a662a9db1a5f1275 --- diff --git a/jsduck.json b/jsduck.json index 6966832c95..6fb4544290 100644 --- a/jsduck.json +++ b/jsduck.json @@ -20,16 +20,16 @@ "resources/src/mediawiki.special", "resources/src/mediawiki.toolbar", "resources/src/mediawiki.widgets", - "resources/src/mediawiki.widgets.visibleByteLimit", + "resources/src/mediawiki.widgets.visibleLengthLimit", "resources/src/jquery/jquery.accessKeyLabel.js", "resources/src/jquery/jquery.byteLength.js", - "resources/src/jquery/jquery.byteLimit.js", "resources/src/jquery/jquery.checkboxShiftClick.js", "resources/src/jquery/jquery.colorUtil.js", "resources/src/jquery/jquery.confirmable.js", "resources/src/jquery/jquery.footHovzer.js", "resources/src/jquery/jquery.getAttrs.js", "resources/src/jquery/jquery.hidpi.js", + "resources/src/jquery/jquery.lengthLimit.js", "resources/src/jquery/jquery.localize.js", "resources/src/jquery/jquery.makeCollapsible.js", "resources/src/jquery/jquery.spinner.js", diff --git a/resources/Resources.php b/resources/Resources.php index f5f17e04fd..bf31024977 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -161,8 +161,8 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'jquery.byteLimit' => [ - 'scripts' => 'resources/src/jquery/jquery.byteLimit.js', - 'dependencies' => 'mediawiki.String', + 'dependencies' => 'jquery.lengthLimit', + 'deprecated' => 'Use "jquery.lengthLimit" instead.', 'targets' => [ 'desktop', 'mobile' ], ], 'jquery.checkboxShiftClick' => [ @@ -268,6 +268,11 @@ return [ ], 'targets' => [ 'desktop', 'mobile' ], ], + 'jquery.lengthLimit' => [ + 'scripts' => 'resources/src/jquery/jquery.lengthLimit.js', + 'dependencies' => 'mediawiki.String', + 'targets' => [ 'desktop', 'mobile' ], + ], 'jquery.localize' => [ 'scripts' => 'resources/src/jquery/jquery.localize.js', ], @@ -1065,7 +1070,7 @@ return [ ], 'dependencies' => [ 'mediawiki.RegExp', - 'jquery.byteLimit', + 'jquery.lengthLimit', ], 'messages' => [ 'htmlform-chosen-placeholder', @@ -1427,7 +1432,7 @@ return [ 'mediawiki.editfont.styles', 'jquery.textSelection', 'oojs-ui-core', - 'mediawiki.widgets.visibleByteLimit', + 'mediawiki.widgets.visibleLengthLimit', 'mediawiki.api', ], ], @@ -2101,7 +2106,7 @@ return [ 'mediawiki.special.movePage' => [ 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.movePage.js', 'dependencies' => [ - 'mediawiki.widgets.visibleByteLimit', + 'mediawiki.widgets.visibleLengthLimit', 'mediawiki.widgets', ], ], @@ -2315,7 +2320,7 @@ return [ ], 'mediawiki.legacy.protect' => [ 'scripts' => 'resources/src/mediawiki.legacy/protect.js', - 'dependencies' => 'jquery.byteLimit', + 'dependencies' => 'jquery.lengthLimit', 'messages' => [ 'protect-unchain-permissions' ] ], // Used in the web installer. Test it after modifying this definition! @@ -2481,12 +2486,17 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.widgets.visibleByteLimit' => [ + 'dependencies' => 'mediawiki.widgets.visibleLengthLimit', + 'deprecated' => 'Use "mediawiki.widgets.visibleLengthLimit" instead.', + 'targets' => [ 'desktop', 'mobile' ] + ], + 'mediawiki.widgets.visibleLengthLimit' => [ 'scripts' => [ - 'resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js' + 'resources/src/mediawiki.widgets.visibleLengthLimit/mediawiki.widgets.visibleLengthLimit.js' ], 'dependencies' => [ 'oojs-ui-core', - 'jquery.byteLimit', + 'jquery.lengthLimit', 'mediawiki.String', ], 'targets' => [ 'desktop', 'mobile' ] diff --git a/resources/src/jquery/jquery.byteLimit.js b/resources/src/jquery/jquery.byteLimit.js deleted file mode 100644 index eb21846935..0000000000 --- a/resources/src/jquery/jquery.byteLimit.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @class jQuery.plugin.byteLimit - */ -( function ( $, mw ) { - - var - eventKeys = [ - 'keyup.byteLimit', - 'keydown.byteLimit', - 'change.byteLimit', - 'mouseup.byteLimit', - 'cut.byteLimit', - 'paste.byteLimit', - 'focus.byteLimit', - 'blur.byteLimit' - ].join( ' ' ), - trimByteLength = require( 'mediawiki.String' ).trimByteLength; - - /** - * Utility function to trim down a string, based on byteLimit - * and given a safe start position. It supports insertion anywhere - * in the string, so "foo" to "fobaro" if limit is 4 will result in - * "fobo", not "foba". Basically emulating the native maxlength by - * reconstructing where the insertion occurred. - * - * @method trimByteLength - * @deprecated Use `require( 'mediawiki.String' ).trimByteLength` instead. - * @static - * @param {string} safeVal Known value that was previously returned by this - * function, if none, pass empty string. - * @param {string} newVal New value that may have to be trimmed down. - * @param {number} byteLimit Number of bytes the value may be in size. - * @param {Function} [fn] See jQuery#byteLimit. - * @return {Object} - * @return {string} return.newVal - * @return {boolean} return.trimmed - */ - mw.log.deprecate( $, 'trimByteLength', trimByteLength, - 'Use require( \'mediawiki.String\' ).trimByteLength instead.', '$.trimByteLength' ); - - /** - * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well, - * when, for example, a database field has a byte limit rather than a character limit. - * Plugin rationale: Browser has native maxlength for number of characters, this plugin - * exists to limit number of bytes instead. - * - * Can be called with a custom limit (to use that limit instead of the maxlength attribute - * value), a filter function (in case the limit should apply to something other than the - * exact input value), or both. Order of parameters is important! - * - * @param {number} [limit] Limit to enforce, fallsback to maxLength-attribute, - * called with fetched value as argument. - * @param {Function} [fn] Function to call on the string before assessing the length. - * @return {jQuery} - * @chainable - */ - $.fn.byteLimit = function ( limit, fn ) { - // If the first argument is the function, - // set fn to the first argument's value and ignore the second argument. - if ( $.isFunction( limit ) ) { - fn = limit; - limit = undefined; - // Either way, verify it is a function so we don't have to call - // isFunction again after this. - } else if ( !fn || !$.isFunction( fn ) ) { - fn = undefined; - } - - // The following is specific to each element in the collection. - return this.each( function ( i, el ) { - var $el, elLimit, prevSafeVal; - - $el = $( el ); - - // If no limit was passed to byteLimit(), use the maxlength value. - // Can't re-use 'limit' variable because it's in the higher scope - // that would affect the next each() iteration as well. - // Note that we use attribute to read the value instead of property, - // because in Chrome the maxLength property by default returns the - // highest supported value (no indication that it is being enforced - // by choice). We don't want to bind all of this for some ridiculously - // high default number, unless it was explicitly set in the HTML. - // Also cast to a (primitive) number (most commonly because the maxlength - // attribute contains a string, but theoretically the limit parameter - // could be something else as well). - elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit ); - - // If there is no (valid) limit passed or found in the property, - // skip this. The < 0 check is required for Firefox, which returns - // -1 (instead of undefined) for maxLength if it is not set. - if ( !elLimit || elLimit < 0 ) { - return; - } - - if ( fn ) { - // Save function for reference - $el.data( 'byteLimit.callback', fn ); - } - - // Remove old event handlers (if there are any) - $el.off( '.byteLimit' ); - - if ( fn ) { - // Disable the native maxLength (if there is any), because it interferes - // with the (differently calculated) byte limit. - // Aside from being differently calculated (average chars with byteLimit - // is lower), we also support a callback which can make it to allow longer - // values (e.g. count "Foo" from "User:Foo"). - // maxLength is a strange property. Removing or setting the property to - // undefined directly doesn't work. Instead, it can only be unset internally - // by the browser when removing the associated attribute (Firefox/Chrome). - // https://bugs.chromium.org/p/chromium/issues/detail?id=136004 - $el.removeAttr( 'maxlength' ); - - } else { - // If we don't have a callback the bytelimit can only be lower than the charlimit - // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce - // the native limit for efficiency when possible (it will make the while-loop below - // faster by there being less left to interate over). - $el.attr( 'maxlength', elLimit ); - } - - // Safe base value, used to determine the path between the previous state - // and the state that triggered the event handler below - and enforce the - // limit approppiately (e.g. don't chop from the end if text was inserted - // at the beginning of the string). - prevSafeVal = ''; - - // We need to listen to after the change has already happened because we've - // learned that trying to guess the new value and canceling the event - // accordingly doesn't work because the new value is not always as simple as: - // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag - // replacements, and custom input methods and what not. - // Even though we only trim input after it was changed (never prevent it), we do - // listen on events that input text, because there are cases where the text has - // changed while text is being entered and keyup/change will not be fired yet - // (such as holding down a single key, fires keydown, and after each keydown, - // we can trim the previous one). - // See https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for - // the order and characteristics of the key events. - $el.on( eventKeys, function () { - var res = trimByteLength( - prevSafeVal, - this.value, - elLimit, - fn - ); - - // Only set value property if it was trimmed, because whenever the - // value property is set, the browser needs to re-initiate the text context, - // which moves the cursor at the end the input, moving it away from wherever it was. - // This is a side-effect of limiting after the fact. - if ( res.trimmed === true ) { - this.value = res.newVal; - // Trigger a 'change' event to let other scripts attached to this node know that the value - // was changed. This will also call ourselves again, but that's okay, it'll be a no-op. - $el.trigger( 'change' ); - } - // Always adjust prevSafeVal to reflect the input value. Not doing this could cause - // trimByteLength to compare the new value to an empty string instead of the - // old value, resulting in trimming always from the end (T42850). - prevSafeVal = res.newVal; - } ); - } ); - }; - - /** - * @class jQuery - * @mixins jQuery.plugin.byteLimit - */ -}( jQuery, mediaWiki ) ); diff --git a/resources/src/jquery/jquery.lengthLimit.js b/resources/src/jquery/jquery.lengthLimit.js new file mode 100644 index 0000000000..00ed7d3edb --- /dev/null +++ b/resources/src/jquery/jquery.lengthLimit.js @@ -0,0 +1,171 @@ +/** + * @class jQuery.plugin.lengthLimit + */ +( function ( $, mw ) { + + var + eventKeys = [ + 'keyup.lengthLimit', + 'keydown.lengthLimit', + 'change.lengthLimit', + 'mouseup.lengthLimit', + 'cut.lengthLimit', + 'paste.lengthLimit', + 'focus.lengthLimit', + 'blur.lengthLimit' + ].join( ' ' ), + trimByteLength = require( 'mediawiki.String' ).trimByteLength; + + /** + * Utility function to trim down a string, based on byteLimit + * and given a safe start position. It supports insertion anywhere + * in the string, so "foo" to "fobaro" if limit is 4 will result in + * "fobo", not "foba". Basically emulating the native maxlength by + * reconstructing where the insertion occurred. + * + * @method trimByteLength + * @deprecated Use `require( 'mediawiki.String' ).trimByteLength` instead. + * @static + * @param {string} safeVal Known value that was previously returned by this + * function, if none, pass empty string. + * @param {string} newVal New value that may have to be trimmed down. + * @param {number} byteLimit Number of bytes the value may be in size. + * @param {Function} [fn] See jQuery#byteLimit. + * @return {Object} + * @return {string} return.newVal + * @return {boolean} return.trimmed + */ + mw.log.deprecate( $, 'trimByteLength', trimByteLength, + 'Use require( \'mediawiki.String\' ).trimByteLength instead.', '$.trimByteLength' ); + + /** + * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well, + * when, for example, a database field has a byte limit rather than a character limit. + * Plugin rationale: Browser has native maxlength for number of characters, this plugin + * exists to limit number of bytes instead. + * + * Can be called with a custom limit (to use that limit instead of the maxlength attribute + * value), a filter function (in case the limit should apply to something other than the + * exact input value), or both. Order of parameters is important! + * + * @param {number} [limit] Limit to enforce, fallsback to maxLength-attribute, + * called with fetched value as argument. + * @param {Function} [fn] Function to call on the string before assessing the length. + * @return {jQuery} + * @chainable + */ + $.fn.byteLimit = function ( limit, fn ) { + // If the first argument is the function, + // set fn to the first argument's value and ignore the second argument. + if ( $.isFunction( limit ) ) { + fn = limit; + limit = undefined; + // Either way, verify it is a function so we don't have to call + // isFunction again after this. + } else if ( !fn || !$.isFunction( fn ) ) { + fn = undefined; + } + + // The following is specific to each element in the collection. + return this.each( function ( i, el ) { + var $el, elLimit, prevSafeVal; + + $el = $( el ); + + // If no limit was passed to byteLimit(), use the maxlength value. + // Can't re-use 'limit' variable because it's in the higher scope + // that would affect the next each() iteration as well. + // Note that we use attribute to read the value instead of property, + // because in Chrome the maxLength property by default returns the + // highest supported value (no indication that it is being enforced + // by choice). We don't want to bind all of this for some ridiculously + // high default number, unless it was explicitly set in the HTML. + // Also cast to a (primitive) number (most commonly because the maxlength + // attribute contains a string, but theoretically the limit parameter + // could be something else as well). + elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit ); + + // If there is no (valid) limit passed or found in the property, + // skip this. The < 0 check is required for Firefox, which returns + // -1 (instead of undefined) for maxLength if it is not set. + if ( !elLimit || elLimit < 0 ) { + return; + } + + if ( fn ) { + // Save function for reference + $el.data( 'byteLimit.callback', fn ); + } + + // Remove old event handlers (if there are any) + $el.off( '.byteLimit' ); + + if ( fn ) { + // Disable the native maxLength (if there is any), because it interferes + // with the (differently calculated) byte limit. + // Aside from being differently calculated (average chars with byteLimit + // is lower), we also support a callback which can make it to allow longer + // values (e.g. count "Foo" from "User:Foo"). + // maxLength is a strange property. Removing or setting the property to + // undefined directly doesn't work. Instead, it can only be unset internally + // by the browser when removing the associated attribute (Firefox/Chrome). + // https://bugs.chromium.org/p/chromium/issues/detail?id=136004 + $el.removeAttr( 'maxlength' ); + + } else { + // If we don't have a callback the bytelimit can only be lower than the charlimit + // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce + // the native limit for efficiency when possible (it will make the while-loop below + // faster by there being less left to interate over). + $el.attr( 'maxlength', elLimit ); + } + + // Safe base value, used to determine the path between the previous state + // and the state that triggered the event handler below - and enforce the + // limit approppiately (e.g. don't chop from the end if text was inserted + // at the beginning of the string). + prevSafeVal = ''; + + // We need to listen to after the change has already happened because we've + // learned that trying to guess the new value and canceling the event + // accordingly doesn't work because the new value is not always as simple as: + // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag + // replacements, and custom input methods and what not. + // Even though we only trim input after it was changed (never prevent it), we do + // listen on events that input text, because there are cases where the text has + // changed while text is being entered and keyup/change will not be fired yet + // (such as holding down a single key, fires keydown, and after each keydown, + // we can trim the previous one). + // See https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for + // the order and characteristics of the key events. + $el.on( eventKeys, function () { + var res = trimByteLength( + prevSafeVal, + this.value, + elLimit, + fn + ); + + // Only set value property if it was trimmed, because whenever the + // value property is set, the browser needs to re-initiate the text context, + // which moves the cursor at the end the input, moving it away from wherever it was. + // This is a side-effect of limiting after the fact. + if ( res.trimmed === true ) { + this.value = res.newVal; + // Trigger a 'change' event to let other scripts attached to this node know that the value + // was changed. This will also call ourselves again, but that's okay, it'll be a no-op. + $el.trigger( 'change' ); + } + // Always adjust prevSafeVal to reflect the input value. Not doing this could cause + // trimByteLength to compare the new value to an empty string instead of the + // old value, resulting in trimming always from the end (T42850). + prevSafeVal = res.newVal; + } ); + } ); + }; + + /** + * @class jQuery + * @mixins jQuery.plugin.lengthLimit + */ +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js b/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js deleted file mode 100644 index 03ffca7b95..0000000000 --- a/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js +++ /dev/null @@ -1,31 +0,0 @@ -( function ( mw ) { - - var byteLength = require( 'mediawiki.String' ).byteLength; - - /** - * @class mw.widgets - */ - - /** - * Add a visible byte limit label to a TextInputWidget. - * - * Uses jQuery#byteLimit to enforce the limit. - * - * @param {OO.ui.TextInputWidget} textInputWidget Text input widget - * @param {number} [limit] Byte limit, defaults to $input's maxlength - */ - mw.widgets.visibleByteLimit = function ( textInputWidget, limit ) { - limit = limit || +textInputWidget.$input.attr( 'maxlength' ); - - function updateCount() { - textInputWidget.setLabel( ( limit - byteLength( textInputWidget.getValue() ) ).toString() ); - } - textInputWidget.on( 'change', updateCount ); - // Initialise value - updateCount(); - - // Actually enforce limit - textInputWidget.$input.byteLimit( limit ); - }; - -}( mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets.visibleLengthLimit/mediawiki.widgets.visibleLengthLimit.js b/resources/src/mediawiki.widgets.visibleLengthLimit/mediawiki.widgets.visibleLengthLimit.js new file mode 100644 index 0000000000..03ffca7b95 --- /dev/null +++ b/resources/src/mediawiki.widgets.visibleLengthLimit/mediawiki.widgets.visibleLengthLimit.js @@ -0,0 +1,31 @@ +( function ( mw ) { + + var byteLength = require( 'mediawiki.String' ).byteLength; + + /** + * @class mw.widgets + */ + + /** + * Add a visible byte limit label to a TextInputWidget. + * + * Uses jQuery#byteLimit to enforce the limit. + * + * @param {OO.ui.TextInputWidget} textInputWidget Text input widget + * @param {number} [limit] Byte limit, defaults to $input's maxlength + */ + mw.widgets.visibleByteLimit = function ( textInputWidget, limit ) { + limit = limit || +textInputWidget.$input.attr( 'maxlength' ); + + function updateCount() { + textInputWidget.setLabel( ( limit - byteLength( textInputWidget.getValue() ) ).toString() ); + } + textInputWidget.on( 'change', updateCount ); + // Initialise value + updateCount(); + + // Actually enforce limit + textInputWidget.$input.byteLimit( limit ); + }; + +}( mediaWiki ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 3372bf01fc..785e11462d 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -45,12 +45,12 @@ return [ 'scripts' => [ 'tests/qunit/suites/resources/startup.test.js', 'tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js', - 'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.color.test.js', 'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js', 'tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js', 'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js', 'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js', + 'tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.localize.test.js', 'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js', 'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js', @@ -103,12 +103,12 @@ return [ ], 'dependencies' => [ 'jquery.accessKeyLabel', - 'jquery.byteLimit', 'jquery.color', 'jquery.colorUtil', 'jquery.getAttrs', 'jquery.hidpi', 'jquery.highlightText', + 'jquery.lengthLimit', 'jquery.localize', 'jquery.makeCollapsible', 'jquery.tabIndex', diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js deleted file mode 100644 index d3233da475..0000000000 --- a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js +++ /dev/null @@ -1,286 +0,0 @@ -( function ( $, mw ) { - var simpleSample, U_20AC, poop, mbSample; - - QUnit.module( 'jquery.byteLimit', QUnit.newMwEnvironment() ); - - // Simple sample (20 chars, 20 bytes) - simpleSample = '12345678901234567890'; - - // 3 bytes (euro-symbol) - U_20AC = '\u20AC'; - - // Outside of the BMP (pile of poo emoji) - poop = '\uD83D\uDCA9'; // "💩" - - // Multi-byte sample (22 chars, 26 bytes) - mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; - - // Basic sendkey-implementation - function addChars( $input, charstr ) { - var c, len; - - function x( $input, i ) { - // Add character to the value - return $input.val() + charstr.charAt( i ); - } - - for ( c = 0, len = charstr.length; c < len; c += 1 ) { - $input - .val( x( $input, c ) ) - .trigger( 'change' ); - } - } - - /** - * Test factory for $.fn.byteLimit - * - * @param {Object} options - * @param {string} options.description Test name - * @param {jQuery} options.$input jQuery object in an input element - * @param {string} options.sample Sequence of characters to simulate being - * added one by one - * @param {string} options.expected Expected final value of `$input` - */ - function byteLimitTest( options ) { - var opt = $.extend( { - description: '', - $input: null, - sample: '', - expected: '' - }, options ); - - QUnit.test( opt.description, function ( assert ) { - opt.$input.appendTo( '#qunit-fixture' ); - - // Simulate pressing keys for each of the sample characters - addChars( opt.$input, opt.sample ); - - assert.equal( - opt.$input.val(), - opt.expected, - 'New value matches the expected string' - ); - } ); - } - - byteLimitTest( { - description: 'Plain text input', - $input: $( '' ).attr( 'type', 'text' ), - sample: simpleSample, - expected: simpleSample - } ); - - byteLimitTest( { - description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (T38310)', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit(), - sample: simpleSample, - expected: simpleSample - } ); - - byteLimitTest( { - description: 'Limit using the maxlength attribute', - $input: $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '10' ) - .byteLimit(), - sample: simpleSample, - expected: '1234567890' - } ); - - byteLimitTest( { - description: 'Limit using a custom value', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 10 ), - sample: simpleSample, - expected: '1234567890' - } ); - - byteLimitTest( { - description: 'Limit using a custom value, overriding maxlength attribute', - $input: $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '10' ) - .byteLimit( 15 ), - sample: simpleSample, - expected: '123456789012345' - } ); - - byteLimitTest( { - description: 'Limit using a custom value (multibyte)', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 14 ), - sample: mbSample, - expected: '1234567890' + U_20AC + '1' - } ); - - byteLimitTest( { - description: 'Limit using a custom value (multibyte, outside BMP)', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 3 ), - sample: poop, - expected: '' - } ); - - byteLimitTest( { - description: 'Limit using a custom value (multibyte) overlapping a byte', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 12 ), - sample: mbSample, - expected: '123456789012' - } ); - - byteLimitTest( { - description: 'Pass the limit and a callback as input filter', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 6, function ( val ) { - var title = mw.Title.newFromText( String( val ) ); - // Return without namespace prefix - return title ? title.getMain() : ''; - } ), - sample: 'User:Sample', - expected: 'User:Sample' - } ); - - byteLimitTest( { - description: 'Limit using the maxlength attribute and pass a callback as input filter', - $input: $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '6' ) - .byteLimit( function ( val ) { - var title = mw.Title.newFromText( String( val ) ); - // Return without namespace prefix - return title ? title.getMain() : ''; - } ), - sample: 'User:Sample', - expected: 'User:Sample' - } ); - - byteLimitTest( { - description: 'Pass the limit and a callback as input filter', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 6, function ( val ) { - var title = mw.Title.newFromText( String( val ) ); - // Return without namespace prefix - return title ? title.getMain() : ''; - } ), - sample: 'User:Example', - // The callback alters the value to be used to calculeate - // the length. The altered value is "Exampl" which has - // a length of 6, the "e" would exceed the limit. - expected: 'User:Exampl' - } ); - - byteLimitTest( { - description: 'Input filter that increases the length', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 10, function ( text ) { - return 'prefix' + text; - } ), - sample: simpleSample, - // Prefix adds 6 characters, limit is reached after 4 - expected: '1234' - } ); - - // Regression tests for T43450 - byteLimitTest( { - description: 'Input filter of which the base exceeds the limit', - $input: $( '' ).attr( 'type', 'text' ) - .byteLimit( 3, function ( text ) { - return 'prefix' + text; - } ), - sample: simpleSample, - expected: '' - } ); - - QUnit.test( 'Confirm properties and attributes set', function ( assert ) { - var $el; - - $el = $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '7' ) - .appendTo( '#qunit-fixture' ) - .byteLimit(); - - assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' ); - - $el = $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '7' ) - .appendTo( '#qunit-fixture' ) - .byteLimit( 12 ); - - assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' ); - - $el = $( '' ).attr( 'type', 'text' ) - .attr( 'maxlength', '7' ) - .appendTo( '#qunit-fixture' ) - .byteLimit( 12, function ( val ) { - return val; - } ); - - assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' ); - - $( '' ).attr( 'type', 'text' ) - .addClass( 'mw-test-byteLimit-foo' ) - .attr( 'maxlength', '7' ) - .appendTo( '#qunit-fixture' ); - - $( '' ).attr( 'type', 'text' ) - .addClass( 'mw-test-byteLimit-foo' ) - .attr( 'maxlength', '12' ) - .appendTo( '#qunit-fixture' ); - - $el = $( '.mw-test-byteLimit-foo' ); - - assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' ); - - $el.byteLimit(); - } ); - - QUnit.test( 'Trim from insertion when limit exceeded', function ( assert ) { - var $el; - - // Use a new because the bug only occurs on the first time - // the limit it reached (T42850) - $el = $( '' ).attr( 'type', 'text' ) - .appendTo( '#qunit-fixture' ) - .byteLimit( 3 ) - .val( 'abc' ).trigger( 'change' ) - .val( 'zabc' ).trigger( 'change' ); - - assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 0), not the end' ); - - $el = $( '' ).attr( 'type', 'text' ) - .appendTo( '#qunit-fixture' ) - .byteLimit( 3 ) - .val( 'abc' ).trigger( 'change' ) - .val( 'azbc' ).trigger( 'change' ); - - assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' ); - } ); - - QUnit.test( 'Do not cut up false matching substrings in emoji insertions', function ( assert ) { - var $el, - oldVal = '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" - newVal = '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" - expected = '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9'; // "💩💹💩" - - // Possible bad results: - // * With no surrogate support: - // '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9' "💩💹🢩" - // * With correct trimming but bad detection of inserted text: - // '\uD83D\uDCA9\uD83D\uDCB9\uDCA9' "💩💹�" - - $el = $( '' ).attr( 'type', 'text' ) - .appendTo( '#qunit-fixture' ) - .byteLimit( 12 ) - .val( oldVal ).trigger( 'change' ) - .val( newVal ).trigger( 'change' ); - - assert.strictEqual( $el.val(), expected, 'Pasted emoji correctly trimmed at the end' ); - } ); - - byteLimitTest( { - description: 'Unpaired surrogates do not crash', - $input: $( '' ).attr( 'type', 'text' ).byteLimit( 4 ), - sample: '\uD800\uD800\uDFFF', - expected: '\uD800' - } ); - -}( jQuery, mediaWiki ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js new file mode 100644 index 0000000000..7117d1f420 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js @@ -0,0 +1,286 @@ +( function ( $, mw ) { + var simpleSample, U_20AC, poop, mbSample; + + QUnit.module( 'jquery.lengthLimit', QUnit.newMwEnvironment() ); + + // Simple sample (20 chars, 20 bytes) + simpleSample = '12345678901234567890'; + + // 3 bytes (euro-symbol) + U_20AC = '\u20AC'; + + // Outside of the BMP (pile of poo emoji) + poop = '\uD83D\uDCA9'; // "💩" + + // Multi-byte sample (22 chars, 26 bytes) + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; + + // Basic sendkey-implementation + function addChars( $input, charstr ) { + var c, len; + + function x( $input, i ) { + // Add character to the value + return $input.val() + charstr.charAt( i ); + } + + for ( c = 0, len = charstr.length; c < len; c += 1 ) { + $input + .val( x( $input, c ) ) + .trigger( 'change' ); + } + } + + /** + * Test factory for $.fn.byteLimit + * + * @param {Object} options + * @param {string} options.description Test name + * @param {jQuery} options.$input jQuery object in an input element + * @param {string} options.sample Sequence of characters to simulate being + * added one by one + * @param {string} options.expected Expected final value of `$input` + */ + function byteLimitTest( options ) { + var opt = $.extend( { + description: '', + $input: null, + sample: '', + expected: '' + }, options ); + + QUnit.test( opt.description, function ( assert ) { + opt.$input.appendTo( '#qunit-fixture' ); + + // Simulate pressing keys for each of the sample characters + addChars( opt.$input, opt.sample ); + + assert.equal( + opt.$input.val(), + opt.expected, + 'New value matches the expected string' + ); + } ); + } + + byteLimitTest( { + description: 'Plain text input', + $input: $( '' ).attr( 'type', 'text' ), + sample: simpleSample, + expected: simpleSample + } ); + + byteLimitTest( { + description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (T38310)', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit(), + sample: simpleSample, + expected: simpleSample + } ); + + byteLimitTest( { + description: 'Limit using the maxlength attribute', + $input: $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '10' ) + .byteLimit(), + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 10 ), + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value, overriding maxlength attribute', + $input: $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '10' ) + .byteLimit( 15 ), + sample: simpleSample, + expected: '123456789012345' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte)', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 14 ), + sample: mbSample, + expected: '1234567890' + U_20AC + '1' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte, outside BMP)', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 3 ), + sample: poop, + expected: '' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte) overlapping a byte', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 12 ), + sample: mbSample, + expected: '123456789012' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 6, function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Limit using the maxlength attribute and pass a callback as input filter', + $input: $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '6' ) + .byteLimit( function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 6, function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Example', + // The callback alters the value to be used to calculeate + // the length. The altered value is "Exampl" which has + // a length of 6, the "e" would exceed the limit. + expected: 'User:Exampl' + } ); + + byteLimitTest( { + description: 'Input filter that increases the length', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 10, function ( text ) { + return 'prefix' + text; + } ), + sample: simpleSample, + // Prefix adds 6 characters, limit is reached after 4 + expected: '1234' + } ); + + // Regression tests for T43450 + byteLimitTest( { + description: 'Input filter of which the base exceeds the limit', + $input: $( '' ).attr( 'type', 'text' ) + .byteLimit( 3, function ( text ) { + return 'prefix' + text; + } ), + sample: simpleSample, + expected: '' + } ); + + QUnit.test( 'Confirm properties and attributes set', function ( assert ) { + var $el; + + $el = $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit(); + + assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' ); + + $el = $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ); + + assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' ); + + $el = $( '' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12, function ( val ) { + return val; + } ); + + assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' ); + + $( '' ).attr( 'type', 'text' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ); + + $( '' ).attr( 'type', 'text' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '12' ) + .appendTo( '#qunit-fixture' ); + + $el = $( '.mw-test-byteLimit-foo' ); + + assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' ); + + $el.byteLimit(); + } ); + + QUnit.test( 'Trim from insertion when limit exceeded', function ( assert ) { + var $el; + + // Use a new because the bug only occurs on the first time + // the limit it reached (T42850) + $el = $( '' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 3 ) + .val( 'abc' ).trigger( 'change' ) + .val( 'zabc' ).trigger( 'change' ); + + assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 0), not the end' ); + + $el = $( '' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 3 ) + .val( 'abc' ).trigger( 'change' ) + .val( 'azbc' ).trigger( 'change' ); + + assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' ); + } ); + + QUnit.test( 'Do not cut up false matching substrings in emoji insertions', function ( assert ) { + var $el, + oldVal = '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" + newVal = '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" + expected = '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9'; // "💩💹💩" + + // Possible bad results: + // * With no surrogate support: + // '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9' "💩💹🢩" + // * With correct trimming but bad detection of inserted text: + // '\uD83D\uDCA9\uD83D\uDCB9\uDCA9' "💩💹�" + + $el = $( '' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ) + .val( oldVal ).trigger( 'change' ) + .val( newVal ).trigger( 'change' ); + + assert.strictEqual( $el.val(), expected, 'Pasted emoji correctly trimmed at the end' ); + } ); + + byteLimitTest( { + description: 'Unpaired surrogates do not crash', + $input: $( '' ).attr( 'type', 'text' ).byteLimit( 4 ), + sample: '\uD800\uD800\uDFFF', + expected: '\uD800' + } ); + +}( jQuery, mediaWiki ) );