mediawiki-skins-Vector/vector.js
Timo Tijhof 9b2bcbbcae vector.js: Use idle callback (not rAF) for computed style read
requestAnimationFrame (rAF) is called before styles are computed.
Performing a style read there can require a forced style
recalcution which interrupts the browser's natural rendering
life cycle.

To gracefully observe layout of the page without inducing the
cost to render it, it should be accessed after rendering.
A suitable API to schedule time there is requestIdleCallback.

In any event, if the code that needs this measurement executes
very early for some reason, it is still computed on-demand which
means it can't cause any functional regression.

Change-Id: I0d8d3a0b158fa3d9e0895760d0691757f918d91d
2019-09-11 02:18:04 +01:00

97 lines
3.1 KiB
JavaScript

/**
* Vector-specific scripts
*/
/* eslint-disable no-jquery/no-global-selector */
$( function () {
/**
* Collapsible tabs
*/
var $cactions = $( '#p-cactions' ),
$tabContainer = $( '#p-views ul' ),
// Avoid forced style calculation during page load
initialCactionsWidth = function () {
var width = $cactions.width();
initialCactionsWidth = function () {
return width;
};
return width;
};
mw.requestIdleCallback( initialCactionsWidth );
/**
* Focus search input at the very end
*/
$( '#searchInput' ).attr( 'tabindex', $( document ).lastTabIndex() + 1 );
// Bind callback functions to animate our drop down menu in and out
// and then call the collapsibleTabs function on the menu
$tabContainer
.on( 'beforeTabCollapse', function () {
// If the dropdown was hidden, show it
if ( $cactions.hasClass( 'emptyPortlet' ) ) {
$cactions.removeClass( 'emptyPortlet' );
// eslint-disable-next-line no-jquery/no-animate
$cactions.find( 'h3' )
.css( 'width', '1px' )
.animate( { width: initialCactionsWidth() }, 'normal' );
}
} )
.on( 'beforeTabExpand', function () {
// If we're removing the last child node right now, hide the dropdown
if ( $cactions.find( 'li' ).length === 1 ) {
// eslint-disable-next-line no-jquery/no-animate
$cactions.find( 'h3' ).animate( { width: '1px' }, 'normal', function () {
$( this ).attr( 'style', '' )
.parent().addClass( 'emptyPortlet' );
} );
}
} )
.collapsibleTabs( {
expandCondition: function ( eleWidth ) {
// This looks a bit awkward because we're doing expensive queries as late
// as possible.
var distance = $.collapsibleTabs.calculateTabDistance();
// 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.
if ( distance >= eleWidth + 1 ) {
return true;
} else {
// Maybe we can still expand? Account for the width of the "Actions" dropdown
// if the expansion would hide it.
if ( $cactions.find( 'li' ).length === 1 ) {
return distance >= eleWidth + 1 - initialCactionsWidth();
} else {
return false;
}
}
},
collapseCondition: function () {
var collapsibleWidth = 0;
// This looks a bit awkward because we're doing expensive queries as late
// as possible.
// TODO: The dropdown itself should probably "fold" to just the down-arrow
// (hiding the text) if it can't fit on the line?
// Never collapse if there is no overlap.
if ( $.collapsibleTabs.calculateTabDistance() >= 0 ) {
return false;
}
// Always collapse if the "More" button is already shown.
if ( !$cactions.hasClass( 'emptyPortlet' ) ) {
return true;
}
$tabContainer.children( 'li.collapsible' ).each( function ( index, element ) {
collapsibleWidth += $( element ).width();
// Stop this possibly expensive loop the moment the condition is met.
return !( collapsibleWidth > initialCactionsWidth() );
} );
return collapsibleWidth > initialCactionsWidth();
}
} );
} );