Revert "jquery.byteLimit: Partial rewrite to fix logic errors"
authorSiebrand <s.mazeland@xs4all.nl>
Wed, 4 Jul 2012 08:50:02 +0000 (08:50 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 4 Jul 2012 08:50:02 +0000 (08:50 +0000)
This causes bug 38158.

This reverts commit 1ceab34587b98abae29bba9027838ed354330025

resources/jquery/jquery.byteLimit.js
tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js

index e073b71..d8f4bfc 100644 (file)
@@ -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,
                                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();
                                }
                        });
                });
index 60d0092..15fac69 100644 (file)
@@ -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
index 9a039d6..2cb94d1 100644 (file)
@@ -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'
                                );
                                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'
                                );
                description: 'Plain text input',
                $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: $( '<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: $( '<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: $( '<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: $( '<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: $( '<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: $( '<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: $( '<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: $( '<input>' )
                        .attr( 'type', 'text' )
                        .prop( 'maxLength', '6' )
                                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 = $( '<input>' )
+                       .attr( 'type', 'text' )
+                       .prop( 'maxLength', '7' )
+                       .appendTo( '#qunit-fixture' )
+                       .byteLimit();
+
+               strictEqual( $el.prop( 'maxLength' ), 7, 'Pre-set maxLength property unchanged' );
+
+               $el = $( '<input>' )
+                       .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 = $( '<input>' )
+                       .addClass( 'mw-test-byteLimit-foo' )
+                       .attr( 'type', 'text' )
+                       .prop( 'maxLength', '7' )
+                       .appendTo( '#qunit-fixture' );
+
+               $elB = $( '<input>' )
+                       .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 ) );