mediawiki-skins-Vector/resources/skins.vector.js/collapsibleTabs.js
Stephen Niedzielski bd7bd75569 [JavaScript] Validate types
Lift the mists of confusion by checking that all JavaScript types align.
No ignores! This is the JavaScript equivalent to Phan.

This patch adds the necessary infrastructure for verifying typing and
fixes the few flaws found.

Bug: T239262
Change-Id: I2557471421196ea46cd13dfb786a52968fbfcc97
2020-03-16 09:10:08 -06:00

224 lines
7.6 KiB
JavaScript

/** @interface CollapsibleTabsOptions */
( function () {
/** @type {boolean|undefined} */ var boundEvent;
var isRTL = document.documentElement.dir === 'rtl';
var rAF = window.requestAnimationFrame || setTimeout;
$.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', mw.util.debounce( 10, 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,
expandedWidth: 0,
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 () {
$.collapsibleTabs.instances.forEach( function ( $el ) {
var $tab,
data = $.collapsibleTabs.getSettings( $el );
if ( data.shifting ) {
return;
}
// if the two navigations are colliding
if ( $el.children( data.collapsible ).length && data.collapseCondition() ) {
/**
* Fired before tabs are moved to "collapsedContainer".
*
* @event beforeTabCollapse
* @memberof jQuery.plugin.collapsibleTabs
*/
$el.trigger( 'beforeTabCollapse' );
// Move the element to the dropdown menu.
$.collapsibleTabs.moveToCollapsed( $el.children( data.collapsible ).last() );
}
$tab = $( data.collapsedContainer ).children( data.collapsible ).first();
// 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( $tab ).expandedWidth
)
) {
/**
* Fired before tabs are moved to "expandedContainer".
*
* @event beforeTabExpand
* @memberof jQuery.plugin.collapsibleTabs
*/
$el.trigger( 'beforeTabExpand' );
$.collapsibleTabs.moveToExpanded( $tab );
}
} );
},
moveToCollapsed: function ( $moving ) {
/** @type {CollapsibleTabsOptions} */ var outerData;
/** @type {CollapsibleTabsOptions} */ var collapsedContainerSettings;
/** @type {string} */ var target;
outerData = $.collapsibleTabs.getSettings( $moving );
if ( !outerData ) {
return;
}
collapsedContainerSettings = $.collapsibleTabs.getSettings(
$( outerData.expandedContainer )
);
if ( !collapsedContainerSettings ) {
return;
}
collapsedContainerSettings.shifting = true;
// Remove the element from where it's at and put it in the dropdown menu
target = outerData.collapsedContainer;
// eslint-disable-next-line no-jquery/no-animate
$moving.css( 'position', 'relative' )
.css( ( isRTL ? 'left' : 'right' ), 0 )
.animate( { width: '1px' }, 'normal', function () {
$( this ).hide();
// add the placeholder
$( '<span>' ).addClass( 'placeholder' ).css( 'display', 'none' ).insertAfter( this );
$( this ).detach().prependTo( target ).data( 'collapsibleTabsSettings', outerData );
$( this ).attr( 'style', 'display: list-item;' );
collapsedContainerSettings.shifting = false;
rAF( $.collapsibleTabs.handleResize );
} );
},
moveToExpanded: function ( $moving ) {
/** @type {CollapsibleTabsOptions} */ var data;
/** @type {CollapsibleTabsOptions} */ var expandedContainerSettings;
var $target;
var expandedWidth;
data = $.collapsibleTabs.getSettings( $moving );
if ( !data ) {
return;
}
expandedContainerSettings =
$.collapsibleTabs.getSettings( $( data.expandedContainer ) );
if ( !expandedContainerSettings ) {
return;
}
expandedContainerSettings.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(
// eslint-disable-next-line no-jquery/no-animate
$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() || 0;
$moving.data( 'collapsibleTabsSettings', data );
expandedContainerSettings.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' );
}
if ( leftTab && rightTab ) {
leftEnd = leftTab.getBoundingClientRect().right;
rightStart = rightTab.getBoundingClientRect().left;
return rightStart - leftEnd;
}
return 0;
}
};
}() );