mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/Vector.git
synced 2024-12-01 02:37:05 +00:00
75af9ec9b5
The debounce is trailing, not leading/throttled. This means the value doesn't decide how often it runs while the user is resizing. It decides how long after they stop resizing will it first run. Given the calculation isn't super expensive, a much lower value should suffice. The main thing we want is that while the user is actively resizing and 100s of events are queued up (faster than JS can process), that we wait until the end of the chain before computing it (once). If the user actually stops moving, even for a little bit, that little bit however small should be more than long enough for a repaint to take place. Test Plan: * Open two tabs, one after checking out master, and one after checking out this patch. Make sure you are logged-in on MW as an administrator user (to get more tabs). * Verify that `mw.loader.getVersion('skins.vector.js')` returns different values from the console for each. * Resize the window from wide to very narrow and back. Before, the tab bar updates relatively late it feels sluggish. After, the tab bar appears to update as/while you let go. Change-Id: If02338559abc71668d0655e8b3be1a5f73e646a9
232 lines
7.5 KiB
JavaScript
232 lines
7.5 KiB
JavaScript
/**
|
|
* Collapsible Tabs for the Vector skin.
|
|
*
|
|
* @class jQuery.plugin.collapsibleTabs
|
|
*/
|
|
( function () {
|
|
var boundEvent,
|
|
isRTL = document.documentElement.dir === 'rtl',
|
|
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', 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,
|
|
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() ) {
|
|
$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
|
|
)
|
|
) {
|
|
// move the element from the dropdown to the tab
|
|
$el.trigger( 'beforeTabExpand' );
|
|
$.collapsibleTabs.moveToExpanded( $tab );
|
|
}
|
|
} );
|
|
},
|
|
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;
|
|
// 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;' );
|
|
expContainerSettings.shifting = false;
|
|
rAF( $.collapsibleTabs.handleResize );
|
|
} );
|
|
},
|
|
moveToExpanded: function ( $moving ) {
|
|
var data, expContainerSettings, $target, expandedWidth;
|
|
|
|
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(
|
|
// 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();
|
|
$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
|
|
*/
|
|
|
|
}() );
|