X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/skins/vector/collapsibleTabs.js diff --git a/skins/vector/collapsibleTabs.js b/skins/vector/collapsibleTabs.js new file mode 100644 index 00000000..bb5a3524 --- /dev/null +++ b/skins/vector/collapsibleTabs.js @@ -0,0 +1,222 @@ +/** + * Collapsible Tabs for the Vector skin. + * + * @class jQuery.plugin.collapsibleTabs + */ +( function ( $ ) { + var isRTL = document.documentElement.dir === 'rtl', + boundEvent = false, + rAF = window.requestAnimationFrame || setTimeout; + + /** + * @event beforeTabCollapse + */ + + /** + * @event afterTabCollapse + */ + + /** + * @param {Object} [options] + * @param {string} [options.expandedContainer="#p-views ul"] List of tabs + * @param {string} [options.collapsedContainer="#p-cactions ul"] List of menu items + * @param {string} [options.collapsible="li.collapsible"] Match tabs that are collapsible + * @param {Function} [options.expandCondition] + * @param {Function} [options.collapseCondition] + * @return {jQuery} + * @chainable + */ + $.fn.collapsibleTabs = function ( options ) { + // Merge options into the defaults + var settings = $.extend( {}, $.collapsibleTabs.defaults, options ); + + // return if the function is called on an empty jquery object + if ( !this.length ) { + return this; + } + + this.each( function () { + var $el = $( this ); + // add the element to our array of collapsible managers + $.collapsibleTabs.instances.push( $el ); + // attach the settings to the elements + $el.data( 'collapsibleTabsSettings', settings ); + // attach data to our collapsible elements + $el.children( settings.collapsible ).each( function () { + $.collapsibleTabs.addData( $( this ) ); + } ); + } ); + + // if we haven't already bound our resize handler, bind it now + if ( !boundEvent ) { + boundEvent = true; + $( window ).on( 'resize', $.debounce( 100, function () { + rAF( $.collapsibleTabs.handleResize ); + } ) ); + } + + // call our resize handler to setup the page + rAF( $.collapsibleTabs.handleResize ); + return this; + }; + $.collapsibleTabs = { + instances: [], + defaults: { + expandedContainer: '#p-views ul', + collapsedContainer: '#p-cactions ul', + collapsible: 'li.collapsible', + shifting: false, + expandCondition: function ( eleWidth ) { + // If there are at least eleWidth + 1 pixels of free space, expand. + // We add 1 because .width() will truncate fractional values but .offset() will not. + return $.collapsibleTabs.calculateTabDistance() >= eleWidth + 1; + }, + collapseCondition: function () { + // If there's an overlap, collapse. + return $.collapsibleTabs.calculateTabDistance() < 0; + } + }, + addData: function ( $collapsible ) { + var settings = $collapsible.parent().data( 'collapsibleTabsSettings' ); + if ( settings ) { + $collapsible.data( 'collapsibleTabsSettings', { + expandedContainer: settings.expandedContainer, + collapsedContainer: settings.collapsedContainer, + expandedWidth: $collapsible.width() + } ); + } + }, + getSettings: function ( $collapsible ) { + var settings = $collapsible.data( 'collapsibleTabsSettings' ); + if ( !settings ) { + $.collapsibleTabs.addData( $collapsible ); + settings = $collapsible.data( 'collapsibleTabsSettings' ); + } + return settings; + }, + handleResize: function () { + $.each( $.collapsibleTabs.instances, function ( i, $el ) { + var data = $.collapsibleTabs.getSettings( $el ); + if ( data.shifting ) { + return; + } + + // if the two navigations are colliding + if ( $el.children( data.collapsible ).length && data.collapseCondition() ) { + $el.trigger( 'beforeTabCollapse' ); + // move the element to the dropdown menu + $.collapsibleTabs.moveToCollapsed( $el.children( data.collapsible + ':last' ) ); + } + + // if there are still moveable items in the dropdown menu, + // and there is sufficient space to place them in the tab container + if ( $( data.collapsedContainer + ' ' + data.collapsible ).length && + data.expandCondition( $.collapsibleTabs.getSettings( $( data.collapsedContainer ).children( + data.collapsible + ':first' ) ).expandedWidth ) ) { + // move the element from the dropdown to the tab + $el.trigger( 'beforeTabExpand' ); + $.collapsibleTabs + .moveToExpanded( data.collapsedContainer + ' ' + data.collapsible + ':first' ); + } + } ); + }, + moveToCollapsed: function ( $moving ) { + var outerData, expContainerSettings, target; + + outerData = $.collapsibleTabs.getSettings( $moving ); + if ( !outerData ) { + return; + } + expContainerSettings = $.collapsibleTabs.getSettings( $( outerData.expandedContainer ) ); + if ( !expContainerSettings ) { + return; + } + expContainerSettings.shifting = true; + + // Remove the element from where it's at and put it in the dropdown menu + target = outerData.collapsedContainer; + $moving.css( 'position', 'relative' ) + .css( ( isRTL ? 'left' : 'right' ), 0 ) + .animate( { width: '1px' }, 'normal', function () { + $( this ).hide(); + // add the placeholder + $( '' ).insertAfter( this ); + $( this ).detach().prependTo( target ).data( 'collapsibleTabsSettings', outerData ); + $( this ).attr( 'style', 'display: list-item;' ); + expContainerSettings.shifting = false; + rAF( $.collapsibleTabs.handleResize ); + } ); + }, + moveToExpanded: function ( ele ) { + var data, expContainerSettings, $target, expandedWidth, + $moving = $( ele ); + + data = $.collapsibleTabs.getSettings( $moving ); + if ( !data ) { + return; + } + expContainerSettings = $.collapsibleTabs.getSettings( $( data.expandedContainer ) ); + if ( !expContainerSettings ) { + return; + } + expContainerSettings.shifting = true; + + // grab the next appearing placeholder so we can use it for replacing + $target = $( data.expandedContainer ).find( 'span.placeholder:first' ); + expandedWidth = data.expandedWidth; + $moving.css( 'position', 'relative' ).css( ( isRTL ? 'right' : 'left' ), 0 ).css( 'width', '1px' ); + $target.replaceWith( + $moving + .detach() + .css( 'width', '1px' ) + .data( 'collapsibleTabsSettings', data ) + .animate( { width: expandedWidth + 'px' }, 'normal', function () { + $( this ).attr( 'style', 'display: block;' ); + rAF( function () { + // Update the 'expandedWidth' in case someone was brazen enough to change the tab's + // contents after the page load *gasp* (T71729). This doesn't prevent a tab from + // collapsing back and forth once, but at least it won't continue to do that forever. + data.expandedWidth = $moving.width(); + $moving.data( 'collapsibleTabsSettings', data ); + expContainerSettings.shifting = false; + $.collapsibleTabs.handleResize(); + } ); + } ) + ); + }, + /** + * Get the amount of horizontal distance between the two tabs groups in pixels. + * + * Uses `#left-navigation` and `#right-navigation`. If negative, this + * means that the tabs overlap, and the value is the width of overlapping + * parts. + * + * Used in default `expandCondition` and `collapseCondition` options. + * + * @return {number} distance/overlap in pixels + */ + calculateTabDistance: function () { + var leftTab, rightTab, leftEnd, rightStart; + + // In RTL, #right-navigation is actually on the left and vice versa. + // Hooray for descriptive naming. + if ( !isRTL ) { + leftTab = document.getElementById( 'left-navigation' ); + rightTab = document.getElementById( 'right-navigation' ); + } else { + leftTab = document.getElementById( 'right-navigation' ); + rightTab = document.getElementById( 'left-navigation' ); + } + + leftEnd = leftTab.getBoundingClientRect().right; + rightStart = rightTab.getBoundingClientRect().left; + return rightStart - leftEnd; + } + }; + + /** + * @class jQuery + * @mixins jQuery.plugin.collapsibleTabs + */ + +}( jQuery ) );