From 1fe6a6c6e5505e52b505d9ccfcf0ed14374b21c3 Mon Sep 17 00:00:00 2001 From: Siebrand Date: Wed, 4 Jul 2012 08:50:02 +0000 Subject: [PATCH] Revert "jquery.byteLimit: Partial rewrite to fix logic errors" This causes bug 38158. This reverts commit 1ceab34587b98abae29bba9027838ed354330025 --- resources/jquery/jquery.byteLimit.js | 95 ++++----- .../jquery/jquery.byteLength.test.js | 4 +- .../resources/jquery/jquery.byteLimit.test.js | 185 +++++++++++------- 3 files changed, 169 insertions(+), 115 deletions(-) diff --git a/resources/jquery/jquery.byteLimit.js b/resources/jquery/jquery.byteLimit.js index e073b715b4..d8f4bfc50e 100644 --- a/resources/jquery/jquery.byteLimit.js +++ b/resources/jquery/jquery.byteLimit.js @@ -7,23 +7,20 @@ ( function ( $, undefined ) { /** - * 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. + * Enforces a byte limit to a textbox, 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! + * 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 arguments is important! * - * @context {jQuery} Instance of jQuery for one or more input elements. - * @param limit {Number} [optional] Limit to enforce, fallsback to - * maxLength-attribute, called with fetched value as argument. - * @param fn {Function} [optional] Function to call on the input string - * before assessing the length. - * @return {jQuery} The context. + * @context {jQuery} Instance of jQuery for one or more input elements + * @param limit {Number} [optional] Limit to enforce, fallsback to maxLength-attribute, + * called with fetched value as argument. + * @param fn {Function} [optional] Function to call on the input string before assessing the length + * @return {jQuery} The context */ $.fn.byteLimit = function ( limit, fn ) { // If the first argument is the function, @@ -51,41 +48,49 @@ return; } + // Update/set attribute value, but only if there is no callback set. // If there's a callback set, it's possible that the limit being enforced - // is lower or higher (ie. if the callback would return "Foo" for "User:Foo"). + // is too low (ie. if the callback would return "Foo" for "User:Foo"). // Usually this isn't a problem since browsers ignore maxLength when setting - // the value property through JavaScript, but Safari 4 violates that rule, - // and makes sense to generally make sure the native browser limit doesn't - // interfere - $el.removeProp( 'maxLength' ); + // the value property through JavaScript, but Safari 4 violates that rule, so + // we have to remove or not set the property if we have a callback. + if ( fn === undefined ) { + $el.prop( 'maxLength', elLimit ); + } else { + $el.removeProp( 'maxLength' ); + } // Save function for reference - $el.data( 'byteLimitCallback', fn ); + $el.data( 'byteLimit-callback', fn ); - // Using keyup instead of keypress so that we don't have to account - // for the infinite number of methods for character insertion (typing, - // holding down for multiple characters, special characters inserted - // with shift/alt, backspace, drag/drop, cut/copy/paste, selecting - // text and replacing). - // Also using onchange. Usually only triggered when field loses focus, - // (incl. before submission), which seems redundant. But this is used - // to allow other JavaScript libraries (e.g. for custom input methods of - // special characters) which tend to trigger onchange as convienience for - // plugins like these. - $el.on( 'keyup change', function ( e ) { - var len, - $el = $( this ), - curVal = $el.val(), - val = curVal; - - // Run any value modifier (e.g. a function to apply the limit to - // "Foo" in value "User:Foo"). - while ( $.byteLength( fn ? fn( val ) : val ) > elLimit ) { - val = val.substr( 0, val.length - 1 ); - }; - - if ( val !== curVal ) { - $el.val( val ); + // We've got something, go for it: + $el.keypress( function ( e ) { + var val, len, charLen; + // First check to see if this is actually a character key + // being pressed. + // Based on key-event info from http://unixpapa.com/js/key.html + // jQuery should also normalize e.which to be consistent cross-browser, + // however the same check is still needed regardless of jQuery. + + // Note: At the moment, for some older opera versions (~< 10.5) + // some special keys won't be recognized (aka left arrow key). + // Backspace will be, so not big issue. + + if ( e.which === 0 || e.charCode === 0 || e.which === 8 || + e.ctrlKey || e.altKey || e.metaKey ) + { + // A special key (backspace, etc) so don't interfere + return true; + } + + val = fn !== undefined ? fn( $( this ).val() ): $( this ).val(); + len = $.byteLength( val ); + // Note that keypress returns a character code point, not a keycode. + // However, this may not be super reliable depending on how keys come in... + charLen = $.byteLength( String.fromCharCode( e.which ) ); + + if ( ( len + charLen ) > elLimit ) { + e.preventDefault(); } }); }); diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js index 60d009266e..15fac69119 100644 --- a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js @@ -5,7 +5,7 @@ test( '-- Initial check', function() { ok( $.byteLength, 'jQuery.byteLength defined' ); } ); -test( 'Simple text', function () { +test( 'Simple text', function() { expect(5); var azLc = 'abcdefghijklmnopqrstuvwxyz', @@ -22,7 +22,7 @@ test( 'Simple text', function () { } ); -test( 'Special text', function () { +test( 'Special text', window.foo = function() { expect(5); // http://en.wikipedia.org/wiki/UTF-8 diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js index 9a039d6d68..2cb94d1b94 100644 --- a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js @@ -1,72 +1,75 @@ ( function ( $ ) { - var sample20simple, sample1mb, sample22mixed; + var simpleSample, U_20AC, mbSample; module( 'jquery.byteLimit', QUnit.newMwEnvironment() ); // Simple sample (20 chars, 20 bytes) - sample20simple = '1234567890abcdefghij'; + simpleSample = '12345678901234567890'; - // Euro-symbol (1 char, 3 bytes) - sample1mb = '\u20AC'; + // 3 bytes (euro-symbol) + U_20AC = '\u20AC'; // Multi-byte sample (22 chars, 26 bytes) - sample22mixed = '1234567890' + sample1mb + 'abcdefghij' + sample1mb; + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; - /** - * Basic emulation of character-by-charater insertion - * and triggering of keyup event after each character. - */ - function simulateKeyUps( $input, charstr ) { - var i, code, event, liveVal, - len = charstr.length; + // Basic sendkey-implementation + function addChars( $input, charstr ) { + var len, i, prevVal, code, event; + len = charstr.length; for ( i = 0; i < len; i += 1 ) { - // Always use the live value as base - liveVal = $input.val(); + // Keep track of the previous value + prevVal = $input.val(); - // Get the key code for the to-be-inserted character + // Get the key code code = charstr.charCodeAt( i ); - $input.val( liveVal + charstr.charAt( i ) ); - - // Trigger keyup event - event = new jQuery.Event( 'keyup', { + // Trigger event and undo if prevented + event = new jQuery.Event( 'keypress', { + which: code, keyCode: code, charCode: code } ); $input.trigger( event ); + if ( !event.isDefaultPrevented() ) { + $input.val( prevVal + charstr.charAt( i ) ); + } } } /** * Test factory for $.fn.byteLimit * - * @param Object options + * @param $input {jQuery} jQuery object in an input element + * @param hasLimit {Boolean} Wether a limit should apply at all + * @param limit {Number} Limit (if used) otherwise undefined + * The limit should be less than 20 (the sample data's length) */ function byteLimitTest( options ) { var opt = $.extend({ description: '', $input: null, sample: '', - limit: false, - expected: '' + hasLimit: false, + expected: '', + limit: null }, options); test( opt.description, function () { - var rawVal, fn, useVal; + var rawVal, fn, newVal; opt.$input.appendTo( '#qunit-fixture' ); - simulateKeyUps( opt.$input, opt.sample ); - + // Simulate pressing keys for each of the sample characters + addChars( opt.$input, opt.sample ); rawVal = opt.$input.val(); - fn = opt.$input.data( 'byteLimitCallback' ); - useVal = $.isFunction( fn ) ? fn( rawVal ) : rawVal; + fn = opt.$input.data( 'byteLimit-callback' ); + newVal = $.isFunction( fn ) ? fn( rawVal ) : rawVal; - if ( opt.limit ) { - expect( 3 ); + if ( opt.hasLimit ) { + expect(3); QUnit.ltOrEq( - $.byteLength( useVal ), + $.byteLength( newVal ), opt.limit, 'Prevent keypresses after byteLimit was reached, length never exceeded the limit' ); @@ -78,10 +81,10 @@ equal( rawVal, opt.expected, 'New value matches the expected string' ); } else { - expect( 2 ); - equal( useVal, opt.expected, 'New value matches the expected string' ); + expect(2); + equal( newVal, opt.expected, 'New value matches the expected string' ); equal( - $.byteLength( useVal ), + $.byteLength( newVal ), $.byteLength( opt.expected ), 'Unlimited scenarios are not affected, expected length reached' ); @@ -98,101 +101,99 @@ description: 'Plain text input', $input: $( '' ) .attr( 'type', 'text' ), - sample: sample20simple, - expected: sample20simple + sample: simpleSample, + hasLimit: false, + expected: simpleSample }); byteLimitTest({ - description: 'No .byteLimit() parameters and no maxLength property - should not throw exceptions (bug 36310)', + description: 'Plain text input. Calling byteLimit with no parameters and no maxLength property (bug 36310)', $input: $( '' ) .attr( 'type', 'text' ) .byteLimit(), - sample: sample20simple, - expected: sample20simple + sample: simpleSample, + hasLimit: false, + expected: simpleSample }); byteLimitTest({ - description: 'maxLength property', + description: 'Limit using the maxlength attribute', $input: $( '' ) .attr( 'type', 'text' ) .prop( 'maxLength', '10' ) .byteLimit(), - sample: sample20simple, + sample: simpleSample, + hasLimit: true, limit: 10, expected: '1234567890' }); byteLimitTest({ - description: '.byteLimit( limit )', + description: 'Limit using a custom value', $input: $( '' ) .attr( 'type', 'text' ) .byteLimit( 10 ), - sample: sample20simple, + sample: simpleSample, + hasLimit: true, limit: 10, expected: '1234567890' }); byteLimitTest({ - description: 'Limit passed to .byteLimit() takes precedence over maxLength property', + description: 'Limit using a custom value, overriding maxlength attribute', $input: $( '' ) .attr( 'type', 'text' ) .prop( 'maxLength', '10' ) .byteLimit( 15 ), - sample: sample20simple, + sample: simpleSample, + hasLimit: true, limit: 15, - expected: '1234567890abcde' + expected: '123456789012345' }); byteLimitTest({ - description: '.byteLimit( limit ) - mixed - cut in simplepart', + description: 'Limit using a custom value (multibyte)', $input: $( '' ) .attr( 'type', 'text' ) .byteLimit( 14 ), - sample: sample22mixed, + sample: mbSample, + hasLimit: true, limit: 14, - expected: '1234567890' + sample1mb + 'a' + expected: '1234567890' + U_20AC + '1' }); byteLimitTest({ - description: '.byteLimit( limit ) - mixed - cut in multibyte', + description: 'Limit using a custom value (multibyte) overlapping a byte', $input: $( '' ) .attr( 'type', 'text' ) .byteLimit( 12 ), - sample: sample22mixed, + sample: mbSample, + hasLimit: true, limit: 12, - // The 3-byte symbol after 0 would have exceeded the 12 byte limit. - // Later when the simulation resumed typing, the two simple characters - // were allowed. - expected: '1234567890' + 'ab' + expected: '1234567890' + '12' }); byteLimitTest({ - description: '.byteLimit( limit, fn ) callback can alter the value to be checked against', + description: 'Pass the limit and a callback as input filter', $input: $( '' ) .attr( 'type', 'text' ) .byteLimit( 6, function ( val ) { - // Only construct mw.Title if the string is non-empty, - // since mw.Title throws an exception on invalid titles. + // Invalid title if ( val === '' ) { return ''; } - // Example: Use value without namespace prefix. + // Return without namespace prefix return new mw.Title( String( val ) ).getMain(); } ), - sample: 'User:John', - // Limit is 6, text "User:Sample" would normally be too long, - // but in this case we test the callback's ability to only - // apply the limit to part of the input. The part "John" in this - // case is within the limit. - limit: 6, - // The callback only affects the comparison, the actual input field - // should still contain the full value. - expected: 'User:John' + sample: 'User:Sample', + hasLimit: true, + limit: 6, // 'Sample' length + expected: 'User:Sample' }); byteLimitTest({ - description: '.byteLimit( fn ) combined with maxLength property', + description: 'Limit using the maxlength attribute and pass a callback as input filter', $input: $( '' ) .attr( 'type', 'text' ) .prop( 'maxLength', '6' ) @@ -206,8 +207,56 @@ return new mw.Title( String( val ) ).getMain(); } ), sample: 'User:Sample', + hasLimit: true, limit: 6, // 'Sample' length expected: 'User:Sample' }); + test( 'Confirm properties and attributes set', function () { + var $el, $elA, $elB; + + expect(5); + + $el = $( '' ) + .attr( 'type', 'text' ) + .prop( 'maxLength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit(); + + strictEqual( $el.prop( 'maxLength' ), 7, 'Pre-set maxLength property unchanged' ); + + $el = $( '' ) + .attr( 'type', 'text' ) + .prop( 'maxLength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ); + + strictEqual( $el.prop( 'maxLength' ), 12, 'maxLength property updated if value was passed to $.fn.byteLimit' ); + + $elA = $( '' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'type', 'text' ) + .prop( 'maxLength', '7' ) + .appendTo( '#qunit-fixture' ); + + $elB = $( '' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'type', 'text' ) + .prop( 'maxLength', '12' ) + .appendTo( '#qunit-fixture' ); + + $el = $( '.mw-test-byteLimit-foo' ); + + strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' ); + + $el.byteLimit(); + + // Before bug 35294 was fixed, both $elA and $elB had maxLength set to 7, + // because $.fn.byteLimit sets: + // `limit = limitArg || this.prop( 'maxLength' ); this.prop( 'maxLength', limit )` + // and did so outside the each() loop. + strictEqual( $elA.prop( 'maxLength' ), 7, 'maxLength was not incorrectly set on #1 when calling byteLimit on multiple elements (bug 35294)' ); + strictEqual( $elB.prop( 'maxLength' ), 12, 'maxLength was not incorrectly set on #2 when calling byteLimit on multiple elements (bug 35294)' ); + }); + }( jQuery ) ); -- 2.20.1