Page MenuHomePhabricator

jquery.makeCollapsible.js

Authored By
bzimport
Nov 22 2014, 12:16 AM
Size
15 KB
Referenced Files
None
Subscribers
None

jquery.makeCollapsible.js

/**
* 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".
*
* @author Krinkle <krinklemail@gmail.com>
* @author Lupo https://commons.wikimedia.org/wiki/User:Lupo
*
* Dual license:
* @license CC-BY 3.0 <http://creativecommons.org/licenses/by/3.0>
* @license GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
*/
( function( $, mw ) {
// Warning: this code employs a number of speed optimizations to avoid excessive runtime on older
// browsers to set up the initial state if there are many collapsible, initially collapsed elements
// on the page.
// A particular problem is that this module uses classes to record the collapsed/expanded state of
// items. Modifying classes may cause reflows of the page rendering, which may make subsequent hide
// operations slow because they need to recalculate the computed styles of elements. Also, simply
// triggering the installed events to actually collapse the elements may cause significant overhead,
// even if event bubbling is bypassed through triggerHandler() instead of trigger(). Finally, when
// setting up the initial collapsed elements, no animations must be used.
// The code below takes care of all this. Initial setup runs in two phases: in a first phase, we only
// collect all the modifications in a "data" object without making any changes to the DOM; then in a
// second phase, we play back all the recorded modifications. During phase one, we do not trigger events
// on togglers but invoke the installed toggle* function directly.
// Once the setup is done, normal event handling will typically invoke events without "data" objects,
// and will then perform DOM modifications right away, which should be OK since any click on a toggler
// will change only one collapsible element, in which case the performance should be still good enough.
// Performance becomes only a problem if we want to toggle several collapsible elements at once.
// Because we know that we only have a "data" object during initial setup, where we at most want to collapse
// some elements, the handling of collapse/expand is slightly asymmetric in the toggle* handlers as we apply
// class changes directly when expanding.
var rspace = /\s+/;
// Remove and add some classes in one step.
$.fn.changeClass = function( fromClass, toClass ) {
if ( !fromClass ) {
return this.addClass( toClass );
}
if ( !toClass ) {
return this.removeClass( fromClass );
}
var classNames, fromNames = fromClass.split( rspace ), toNames = toClass.split( rspace ), elem, classes, i, j, l;
for ( i = 0, l = this.length; i < l; i++ ) {
elem = this[i];
classNames = ( elem.className || "" ).split ( ' ' ) || [];
classes = {};
for ( j = 0; j < classNames.length; j++ ) {
if ( classNames[j] ) {
classes[classNames[j]] = true;
}
}
for ( j = 0; j < fromNames.length; j++ ) {
if ( fromNames[j] && classes[fromNames[j]] ) {
classes[fromNames[j]] = false;
}
}
for ( j = 0; j < toNames.length; j++ ) {
if ( toNames[j] ) {
classes[toNames[j]] = true;
}
}
classNames = "";
for ( j in classes ) {
if ( classes[j] ) {
classNames += j + ' ';
}
}
elem.className = jQuery.trim( classNames );
classes = null;
}
return this;
};
function collapse( $collapsible, $defaultToggle, immediateTransition, data ) {
var $containers;
var op = 'slide';
if ( !data ) {
data = { immediate: [], fade: [], slide: [] }; // Use plain arrays.
}
if ( $collapsible.is( 'table' ) ) {
// Hide all table rows of this table
// Slide doesn't work with tables, but fade does as of jQuery 1.1.3
// http://stackoverflow.com/questions/467336#920480
$containers = $collapsible.children( 'tbody' ).children ( 'tr' );
if ( $defaultToggle ) {
// Exclude table row containing toggle link
$containers = $containers.not( $defaultToggle.parent().parent() );
}
op = 'fade';
} else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
$containers = $collapsible.children( 'li' );
if ( $defaultToggle ) {
// Exclude list item containing toggle link
$containers = $containers.not( $defaultToggle.parent() );
}
} else { // <div>, <p> etc.
$containers = $collapsible.children( '.mw-collapsible-content' );
if ( !$containers.length ) {
// Otherwise assume this is a customcollapse with a remote toggle
// .. and there is no collapsible-content because the entire element should be toggled
if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
op = 'fade';
}
$containers = $collapsible;
}
}
if ( immediateTransition ) {
op = 'immediate';
}
data[op] = data[op].concat ( jQuery.makeArray( $containers ) );
return data;
}
function expand( $collapsible, $defaultToggle, immediateTransition, data ) {
var $containers;
var op = 'slide';
if (!data) {
data = { immediate: [], fade: [], slide: [] };
}
if ( $collapsible.is( 'table' ) ) {
$containers = $collapsible.children( 'tbody' ).children ( 'tr' );
if ( $defaultToggle ) {
// Exclude table row containing toggle
$containers = $containers.not( $defaultToggle.parent().parent() );
}
op = 'fade';
} else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
$containers = $collapsible.children( 'li' );
if ( $defaultToggle ) {
// Exclude list-item containing togglelink
$containers = $containers.not( $defaultToggle.parent() );
}
} else { // <div>, <p> etc.
$containers = $collapsible.children( '.mw-collapsible-content' );
// If a collapsible-content is defined, collapse it
if (! $containers.length ) {
if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
op = 'fade'
}
$containers = $collapsible;
}
}
if ( immediateTransition ) {
op = 'immediate';
}
data[op] = data[op].concat ( jQuery.makeArray( $containers ) );
return data;
}
function hide( data ) {
$( data.immediate ).hide();
$( data.fade ).stop( true, true ).fadeOut();
$( data.slide ).stop( true, true ).slideUp();
}
function show( data ) {
$( data.immediate ).show();
$( data.fade ).stop( true, true ).fadeIn();
$( data.slide ).stop( true, true ).slideDown();
}
function toggleLinkPremade( $that, e, $collapsible, data ) {
if (!data) {
data = { immediate: false, doIt: true };
}
if ( data.doIt ) {
$collapsible.toggleClass( 'mw-collapsed' );
}
if ( e ) {
if ( $(e.target).is('a') ) {
return true;
}
e.preventDefault();
e.stopPropagation();
}
// It's expanded right now
if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
// Change toggle to collapsed
if ( data.toggles ) {
data.toggles = data.toggles.concat( jQuery.makeArray( $that ) );
} else {
$that.changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
}
// Collapse element
data.lists = collapse( $collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
hide( data.lists );
}
// It's collapsed right now
} else {
// Change toggle to expanded
$that.changeClass( 'mw-collapsible-toggle-collapsed', 'mw-collapsible-toggle-expanded' );
// Expand element
data.lists = expand( $collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
show( data.lists );
}
}
}
function toggleLinkCustom( $that, e, $collapsible, data ) {
if (!data) {
data = { immediate: false, doIt: true };
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
// Get current state and toggle to the opposite
if ( data.doIt ) {
$collapsible.toggleClass( 'mw-collapsed' );
}
if ( !data.doIt || $collapsible.hasClass( 'mw-collapsed' ) ) {
data.lists = collapse( $collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
hide( data.lists );
}
} else {
data.lists = expand( $collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
show( data.lists );
}
}
}
$.fn.makeCollapsible = function() {
var defaultCollapseText = mw.msg( 'collapsible-collapse' );
var defaultExpandText = mw.msg( 'collapsible-expand' );
var data = { immediate: true, doIt: false, toggles: [], change: [] }; // Record changes; use plain arrays
var $result = this
.not( '.mw-made-collapsible' ) // Exclude already handled elements
.addClass ( 'mw-collapsible mw-made-collapsible' ) // case: $( '#myAJAXelement' ).makeCollapsible()
.each(function() {
var _fn = 'jquery.makeCollapsible> ';
// Define reused variables and functions
var $that = $(this),
that = this,
collapsetext = $that.attr( 'data-collapsetext' ) || defaultCollapseText,
expandtext = $that.attr( 'data-expandtext' ) || defaultExpandText;
function toggleLinkDefault( $that, e, $collapsible, data ) {
if (!data) {
data = { immediate: false, doIt: true, toggles : []};
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
if ( data.doIt ) {
$collapsible.toggleClass( 'mw-collapsed' );
}
// It's expanded right now
if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
// Change link to "Show"
if ( data.toggles ) {
data.toggles = data.toggles.concat (jQuery.makeArray( $that ) );
data.change[data.change.length] = { $item: $that, text: "" + expandtext };
} else {
$that.changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
var $lks = $that.children( 'a' );
( $lks.length ? $lks : $that ).text( expandtext );
}
// Collapse element
data.lists = collapse ($collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
hide( data.lists );
}
// It's collapsed right now
} else {
// Change link to "Hide"
$that.changeClass( 'mw-collapsible-toggle-collapsed', 'mw-collapsible-toggle-expanded' );
var $lks = $that.children( 'a' );
( $lks.length ? $lks : $that ).text( collapsetext );
// Expand element
data.lists = expand( $collapsible, $that, data.immediate, data.lists );
if ( data.doIt ) {
show( data.lists );
}
}
}
// Create toggle link with a space around the brackets (&nbsp;[text]&nbsp;)
function createToggleLink ($collapsible) {
return $( '<a href="#"></a>' )
.text( collapsetext )
.wrap( '<span class="mw-collapsible-toggle"></span>' )
.parent()
.prepend( '&nbsp;[' )
.append( ']&nbsp;' )
.bind( 'click.mw-collapse', function( e ) {
toggleLinkDefault( $(this), e, $collapsible );
} );
}
// Avoid event management overhead by recording in func the installed click handler, and invoke it directly.
// That appears to be faster than even using $.triggerHandler() on the toggle link.
var $toggleLink = null, func;
// 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 ) {
var thatId = $that.attr( 'id' ),
$customTogglers = $( '.' + thatId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) );
mw.log( _fn + 'Found custom collapsible: #' + thatId );
// Double check that there is actually a customtoggle link
if ( $customTogglers.length ) {
$customTogglers.bind( 'click.mw-collapse', function( e ) {
toggleLinkCustom( $(this), e, $that );
} );
$toggleLink = $customTogglers;
} else {
mw.log( _fn + '#' + thatId + ': Missing toggler!' );
}
func = toggleLinkCustom;
// If this is not a custom case, do the default: wrap the contents add the toggle link
// Elements are treated differently
} else if ( $that.is( 'table' ) ) {
// The toggle-link will be in one the the cells (td or th) of the first row
var $firstRowCells = $that.children ( 'tbody' ).children( 'tr:first' ).children ('th, td'),
$toggle = $firstRowCells.children( '.mw-collapsible-toggle' );
// If theres no toggle link, add it to the last cell
if ( !$toggle.length ) {
$firstRowCells.eq(-1).prepend( $toggleLink = createToggleLink( $that ) );
func = toggleLinkDefault;
} else {
$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
toggleLinkPremade( $toggle, e, $that );
} );
func = toggleLinkPremade;
}
} else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) {
// The toggle-link will be in the first list-item
var $firstItem = $that.children( 'li:first' ),
$toggle = $firstItem.children( '.mw-collapsible-toggle' );
// If theres no toggle link, add it
if ( !$toggle.length ) {
// 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' );
if ( firstval === undefined || !firstval || firstval == '-1' ) {
$firstItem.attr( 'value', '1' );
}
$toggleLink = createToggleLink( $that );
$that.prepend( $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent() );
func = toggleLinkDefault;
} else {
$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
toggleLinkPremade( $toggle, e, $that );
} );
func = toggleLinkPremade;
}
} else { // <div>, <p> etc.
// The toggle-link will be the first child of the element
var $toggle = $that.children( '.mw-collapsible-toggle' );
// If a direct child .content-wrapper does not exists, create it
if ( !$that.children( '.mw-collapsible-content' ).length ) {
$that.wrapInner( '<div class="mw-collapsible-content"></div>' );
}
// If there's no toggle link, add it
if ( !$toggle.length ) {
$that.prepend( $toggleLink = createToggleLink( $that ) );
func = toggleLinkDefault;
} else {
$toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
toggleLinkPremade( $toggle, e, $that );
} );
func = toggleLinkPremade;
}
}
// If the initial state is collapsed, collapse it.
if ( $that.hasClass( 'mw-collapsed' ) && $toggleLink ) {
func( $toggleLink.eq(0), null, $that, data );
}
} );
// Replay recorded changes. (The "data" closure variable is passed around by reference, so the toggle click handlers can
// return back their changes through it.) data.lists now contains all DOM elements to be collapsed
if ( data.lists ) {
var oldOff = jQuery.fx.off; jQuery.fx.off = true; // Make sure we don't get any animations
hide( data.lists );
jQuery.fx.off = oldOff;
}
$( data.toggles ).changeClass( 'mw-collapsible-toggle-expanded', 'mw-collapsible-toggle-collapsed' );
for ( var i = 0; i < data.change.length; i++ ) {
var $lks = data.change[i].$item.children( 'a' );
if ( !$lks.length ) {
$lks = data.change[i].$item;
}
$lks.text( data.change[i].text );
};
return $result; // Subset of items passed in that were now enabled.
};
} )( jQuery, mediaWiki );

File Metadata

Mime Type
text/x-asm
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
8592
Default Alt Text
jquery.makeCollapsible.js (15 KB)

Event Timeline