From: Timo Tijhof Date: Wed, 29 Aug 2012 12:31:10 +0000 (+0200) Subject: (bug 34876) Make jquery.makeCollapsible less slow. X-Git-Tag: 1.31.0-rc.0~22103 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22articles%22%2C%22id_article=%24id_article%22%29%20.%20%22?a=commitdiff_plain;h=287331a04ec3c45fddb2e522dd8d5819f5cafb93;p=lhc%2Fweb%2Fwiklou.git (bug 34876) Make jquery.makeCollapsible less slow. Several optimisations: * Re-using the same $collapsible jQuery object instead of re-creation of $(this). * Use .data instead of dom manipulation (className property). * Use $.nodeName( HTMLElement, tag ) to check something is , instead of using creating a jQuery object and calling "is.('a')", which goes through a lot of selector stuff. - Fix bug where it says it does instantHide but actually still triggers the event that causes initial animations. Thanks to Lupo for most of these ideas, based on this patch: * https://bugzilla.wikimedia.org/attachment.cgi?id=10200 Verified that these test cases all still work: https://test.wikipedia.org/wiki/User:Krinkle/CollapsingTestpageMw (copy wikitext to localhost) Change-Id: Idb9ca00c03ec7d70903ed7fd79e427efa270ace4 --- diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index b8a7baa43a..529ae4dcfc 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -16,6 +16,7 @@ production. instead. === New features in 1.21 === +* (bug 34876) jquery.makeCollapsible has been improved in performance. === Bug fixes in 1.21 === * (bug 40353) SpecialDoubleRedirect should support interwiki redirects. diff --git a/resources/jquery/jquery.makeCollapsible.js b/resources/jquery/jquery.makeCollapsible.js index 0a4d3645b9..ad156070dd 100644 --- a/resources/jquery/jquery.makeCollapsible.js +++ b/resources/jquery/jquery.makeCollapsible.js @@ -2,13 +2,13 @@ * jQuery makeCollapsible * * This will enable collapsible-functionality on all passed elements. - * Will prevent binding twice to the same element. - * Initial state is expanded by default, this can be overriden by adding class - * "mw-collapsed" to the "mw-collapsible" element. - * Elements made collapsible have class "mw-made-collapsible". - * Except for tables and lists, the inner content is wrapped in "mw-collapsible-content". + * - Will prevent binding twice to the same element. + * - Initial state is expanded by default, this can be overriden by adding class + * "mw-collapsed" to the "mw-collapsible" element. + * - Elements made collapsible have jQuery data "mw-made-collapsible" set to true. + * - The inner content is wrapped in a "div.mw-collapsible-content" (except for tables and lists). * - * @author Krinkle + * @author Krinkle, 2011-2012 * * Dual license: * @license CC-BY 3.0 @@ -21,17 +21,33 @@ $.fn.makeCollapsible = function () { return this.each(function () { // Define reused variables and functions - var $toggle, - lpx = 'jquery.makeCollapsible> ', - $that = $(this).addClass( 'mw-collapsible' ), // case: $( '#myAJAXelement' ).makeCollapsible() - that = this, - collapsetext = $(this).attr( 'data-collapsetext' ), - expandtext = $(this).attr( 'data-expandtext' ), - toggleElement = function ( $collapsible, action, $defaultToggle, instantHide ) { + var lpx = 'jquery.makeCollapsible> ', + collapsible = this, + // Ensure class "mw-collapsible" is present in case .makeCollapsible() + // is called on element(s) that don't have it yet. + $collapsible = $(collapsible).addClass( 'mw-collapsible' ), + collapsetext = $collapsible.attr( 'data-collapsetext' ), + expandtext = $collapsible.attr( 'data-expandtext' ), + $toggle, + $toggleLink, + $firstItem, + collapsibleId, + $customTogglers, + firstval, + /** + * @param {jQuery} $collapsible + * @param {string} action The action this function will take ('expand' or 'collapse'). + * @param {jQuery|null} [optional] $defaultToggle + * @param {Object|undefined} options + */ + toggleElement = function ( $collapsible, action, $defaultToggle, options ) { var $collapsibleContent, $containers; + options = options || {}; // Validate parameters - if ( !$collapsible.jquery ) { // $collapsible must be an instance of jQuery + + // $collapsible must be an instance of jQuery + if ( !$collapsible.jquery ) { return; } if ( action !== 'expand' && action !== 'collapse' ) { @@ -41,7 +57,7 @@ $.fn.makeCollapsible = function () { if ( $defaultToggle === undefined ) { $defaultToggle = null; } - if ( $defaultToggle !== null && !($defaultToggle instanceof $) ) { + if ( $defaultToggle !== null && !$defaultToggle.jquery ) { // is optional (may be undefined), but if defined it must be an instance of jQuery. // If it's not, abort right away. // After this $defaultToggle is either null or a valid jQuery instance. @@ -55,12 +71,12 @@ $.fn.makeCollapsible = function () { // Hide all table rows of this table // Slide doens't work with tables, but fade does as of jQuery 1.1.3 // http://stackoverflow.com/questions/467336#920480 - $containers = $collapsible.find( '>tbody>tr' ); + $containers = $collapsible.find( '> tbody > tr' ); if ( $defaultToggle ) { // Exclude tablerow containing togglelink $containers.not( $defaultToggle.closest( 'tr' ) ).stop(true, true).fadeOut(); } else { - if ( instantHide ) { + if ( options.instantHide ) { $containers.hide(); } else { $containers.stop( true, true ).fadeOut(); @@ -73,19 +89,20 @@ $.fn.makeCollapsible = function () { // Exclude list-item containing togglelink $containers.not( $defaultToggle.parent() ).stop( true, true ).slideUp(); } else { - if ( instantHide ) { + if ( options.instantHide ) { $containers.hide(); } else { $containers.stop( true, true ).slideUp(); } } - } else { //
,

etc. + } else { + //

,

etc. $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' ); // If a collapsible-content is defined, collapse it if ( $collapsibleContent.length ) { - if ( instantHide ) { + if ( options.instantHide ) { $collapsibleContent.hide(); } else { $collapsibleContent.slideUp(); @@ -111,7 +128,7 @@ $.fn.makeCollapsible = function () { // Exclude tablerow containing togglelink $containers.not( $defaultToggle.parent().parent() ).stop(true, true).fadeIn(); } else { - $containers.stop(true, true).fadeIn(); + $containers.stop( true, true ).fadeIn(); } } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) { @@ -123,7 +140,8 @@ $.fn.makeCollapsible = function () { $containers.stop( true, true ).slideDown(); } - } else { //

,

etc. + } else { + //

,

etc. $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' ); // If a collapsible-content is defined, collapse it @@ -142,10 +160,15 @@ $.fn.makeCollapsible = function () { } } }, - // Toggles collapsible and togglelink class and updates text label - toggleLinkDefault = function ( that, e ) { - var $that = $(that), - $collapsible = $that.closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' ); + /** + * Toggles collapsible and togglelink class and updates text label. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + */ + toggleLinkDefault = function ( $that, e, options ) { + var $collapsible = $that.closest( '.mw-collapsible' ).toggleClass( 'mw-collapsed' ); e.preventDefault(); e.stopPropagation(); @@ -159,7 +182,7 @@ $.fn.makeCollapsible = function () { $that.text( expandtext ); } // Collapse element - toggleElement( $collapsible, 'collapse', $that ); + toggleElement( $collapsible, 'collapse', $that, options ); // It's collapsed right now } else { @@ -171,14 +194,20 @@ $.fn.makeCollapsible = function () { $that.text( collapsetext ); } // Expand element - toggleElement( $collapsible, 'expand', $that ); + toggleElement( $collapsible, 'expand', $that, options ); } return; }, - // Toggles collapsible and togglelink class - toggleLinkPremade = function ( $that, e ) { - var $collapsible = $that.eq(0).closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' ); - if ( $(e.target).is( 'a' ) ) { + /** + * Toggles collapsible and togglelink class. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + */ + toggleLinkPremade = function ( $that, e, options ) { + var $collapsible = $that.eq( 0 ).closest( '.mw-collapsible' ).toggleClass( 'mw-collapsed' ); + if ( $.nodeName( e.target, 'a' ) ) { return true; } e.preventDefault(); @@ -189,31 +218,45 @@ $.fn.makeCollapsible = function () { // Change toggle to collapsed $that.removeClass( 'mw-collapsible-toggle-expanded' ).addClass( 'mw-collapsible-toggle-collapsed' ); // Collapse element - toggleElement( $collapsible, 'collapse', $that ); + toggleElement( $collapsible, 'collapse', $that, options ); // It's collapsed right now } else { // Change toggle to expanded $that.removeClass( 'mw-collapsible-toggle-collapsed' ).addClass( 'mw-collapsible-toggle-expanded' ); // Expand element - toggleElement( $collapsible, 'expand', $that ); + toggleElement( $collapsible, 'expand', $that, options ); } return; }, - // Toggles customcollapsible - toggleLinkCustom = function ( $that, e, $collapsible ) { + /** + * Toggles customcollapsible. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + * @param {jQuery} $collapsible + */ + toggleLinkCustom = function ( $that, e, options, $collapsible ) { // For the initial state call of customtogglers there is no event passed - if (e) { + if ( e ) { e.preventDefault(); e.stopPropagation(); } // Get current state and toggle to the opposite var action = $collapsible.hasClass( 'mw-collapsed' ) ? 'expand' : 'collapse'; $collapsible.toggleClass( 'mw-collapsed' ); - toggleElement( $collapsible, action, $that ); + toggleElement( $collapsible, action, $that, options ); }; + // Return if it has been enabled already. + if ( $collapsible.data( 'mw-made-collapsible' ) ) { + return; + } else { + $collapsible.data( 'mw-made-collapsible', true ); + } + // Use custom text or default ? if ( !collapsetext ) { collapsetext = mw.msg( 'collapsible-collapse' ); @@ -223,46 +266,41 @@ $.fn.makeCollapsible = function () { } // Create toggle link with a space around the brackets ( [text] ) - var $toggleLink = + $toggleLink = $( '' ) .text( collapsetext ) .wrap( '' ) - .parent() - .prepend( ' [' ) - .append( '] ' ) - .on( 'click.mw-collapse', function ( e ) { - toggleLinkDefault( this, e ); - } ); - - // Return if it has been enabled already. - if ( $that.hasClass( 'mw-made-collapsible' ) ) { - return; - } else { - $that.addClass( 'mw-made-collapsible' ); - } + .parent() + .prepend( ' [' ) + .append( '] ' ) + .on( 'click.mw-collapse', function ( e, options ) { + toggleLinkDefault( $(this), e, options ); + } ); // Check if this element has a custom position for the toggle link // (ie. outside the container or deeper inside the tree) // Then: Locate the custom toggle link(s) and bind them - if ( ( $that.attr( 'id' ) || '' ).indexOf( 'mw-customcollapsible-' ) === 0 ) { + if ( ( $collapsible.attr( 'id' ) || '' ).indexOf( 'mw-customcollapsible-' ) === 0 ) { - var thatId = $that.attr( 'id' ), - $customTogglers = $( '.' + thatId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) ); - mw.log( lpx + 'Found custom collapsible: #' + thatId ); + collapsibleId = $collapsible.attr( 'id' ); + $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) ); + mw.log( lpx + 'Found custom collapsible: #' + collapsibleId ); // Double check that there is actually a customtoggle link if ( $customTogglers.length ) { - $customTogglers.on( 'click.mw-collapse', function ( e ) { - toggleLinkCustom( $(this), e, $that ); + $customTogglers.on( 'click.mw-collapse', function ( e, options ) { + toggleLinkCustom( $(this), e, options, $collapsible ); } ); } else { - mw.log( lpx + '#' + thatId + ': Missing toggler!' ); + mw.log( lpx + '#' + collapsibleId + ': Missing toggler!' ); } // Initial state - if ( $that.hasClass( 'mw-collapsed' ) ) { - $that.removeClass( 'mw-collapsed' ); - toggleLinkCustom( $customTogglers, null, $that ); + if ( $collapsible.hasClass( 'mw-collapsed' ) ) { + // Remove here so that the toggler goes in the right direction, + // It re-adds the class. + $collapsible.removeClass( 'mw-collapsed' ); + toggleLinkCustom( $customTogglers, null, { instantHide: true }, $collapsible ); } // If this is not a custom case, do the default: @@ -270,23 +308,23 @@ $.fn.makeCollapsible = function () { } else { // Elements are treated differently - if ( $that.is( 'table' ) ) { + if ( $collapsible.is( 'table' ) ) { // The toggle-link will be in one the the cells (td or th) of the first row - var $firstRowCells = $that.find( 'tr:first th, tr:first td' ); - $toggle = $firstRowCells.find( '> .mw-collapsible-toggle' ); + $firstItem = $collapsible.find( 'tr:first th, tr:first td' ); + $toggle = $firstItem.find( '> .mw-collapsible-toggle' ); // If theres no toggle link, add it to the last cell if ( !$toggle.length ) { - $firstRowCells.eq(-1).prepend( $toggleLink ); + $firstItem.eq(-1).prepend( $toggleLink ); } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, options ) { + toggleLinkPremade( $toggle, e, options ); } ); } - } else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) { + } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) { // The toggle-link will be in the first list-item - var $firstItem = $that.find( 'li:first' ); + $firstItem = $collapsible.find( 'li:first' ); $toggle = $firstItem.find( '> .mw-collapsible-toggle' ); // If theres no toggle link, add it @@ -294,46 +332,47 @@ $.fn.makeCollapsible = function () { // Make sure the numeral order doesn't get messed up, force the first (soon to be second) item // to be "1". Except if the value-attribute is already used. // If no value was set WebKit returns "", Mozilla returns '-1', others return null or undefined. - var firstval = $firstItem.attr( 'value' ); + firstval = $firstItem.attr( 'value' ); if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) { $firstItem.attr( 'value', '1' ); } - $that.prepend( $toggleLink.wrap( '

  • ' ).parent() ); + $collapsible.prepend( $toggleLink.wrap( '
  • ' ).parent() ); } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, options ) { + toggleLinkPremade( $toggle, e, options ); } ); } } else { //
    ,

    etc. // The toggle-link will be the first child of the element - $toggle = $that.find( '> .mw-collapsible-toggle' ); + $toggle = $collapsible.find( '> .mw-collapsible-toggle' ); // If a direct child .content-wrapper does not exists, create it - if ( !$that.find( '> .mw-collapsible-content' ).length ) { - $that.wrapInner( '

    ' ); + if ( !$collapsible.find( '> .mw-collapsible-content' ).length ) { + $collapsible.wrapInner( '
    ' ); } // If theres no toggle link, add it if ( !$toggle.length ) { - $that.prepend( $toggleLink ); + $collapsible.prepend( $toggleLink ); } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, options ) { + toggleLinkPremade( $toggle, e, options ); } ); } } } - // Initial state (only for those that are not custom) - if ( $that.hasClass( 'mw-collapsed' ) && ( $that.attr( 'id' ) || '').indexOf( 'mw-customcollapsible-' ) !== 0 ) { - $that.removeClass( 'mw-collapsed' ); + // Initial state (only for those that are not custom, + // because the initial state of those has been taken care of already). + if ( $collapsible.hasClass( 'mw-collapsed' ) && ( $collapsible.attr( 'id' ) || '').indexOf( 'mw-customcollapsible-' ) !== 0 ) { + $collapsible.removeClass( 'mw-collapsed' ); // The collapsible element could have multiple togglers // To toggle the initial state only click one of them (ie. the first one, eq(0) ) // Else it would go like: hide,show,hide,show for each toggle link. - toggleElement( $that, 'collapse', $toggleLink.eq(0), /* instantHide = */ true ); - $toggleLink.eq(0).click(); + // This is just like it would be in reality (only one toggle is clicked at a time). + $toggleLink.eq( 0 ).trigger( 'click', [ { instantHide: true } ] ); } } ); };