User:MM abc.xyz/common.js: Difference between revisions

From Wikimedia Foundation Governance Wiki
Content deleted Content added
Tags: Mobile edit Mobile web edit
 
Replaced content with "'use strict'; // For a detailed explanation regarding each configuration property, visit: // https://jestjs.io/docs/en/configuration.html module.exports = { moduleNameMapper: { '@wikimedia/codex-search': '@wikimedia/codex', '^./templates/(.*).mustache': '<rootDir>/includes/templates/$1.mustache' }, // Automatically clear mock calls and instances between every test clearMocks: true, // Indicates whether the coverage information should be collected while e..."
Tags: Replaced Mobile edit Mobile web edit
 
Line 1: Line 1:
'use strict';
/* eslint-disable no-jquery/no-jquery-constructor, no-jquery/no-other-methods, no-jquery/no-class,
no-jquery/no-extend, no-jquery/no-data, no-jquery/no-css, no-jquery/no-visibility, no-jquery/no-trigger,
no-jquery/no-is-empty-object, no-jquery/no-find-collection, no-jquery/no-attr, no-jquery/no-parent,
no-jquery/no-each-collection */
/**
* This adds behaviour to Vector's tabs in the bottom right so that at smaller
* displays they collapse under the more menu.
*/


// For a detailed explanation regarding each configuration property, visit:
/** @interface CollapsibleTabsOptions */
// https://jestjs.io/docs/en/configuration.html
function init() {
/** @type {boolean|undefined} */ let boundEvent;
const isRTL = document.documentElement.dir === 'rtl';
const rAF = window.requestAnimationFrame || setTimeout;


module.exports = {
// Mark the tabs which can be collapsed under the more menu
moduleNameMapper: {
// eslint-disable-next-line no-jquery/no-global-selector
'@wikimedia/codex-search': '@wikimedia/codex',
$( '#p-views li' )
'^./templates/(.*).mustache': '<rootDir>/includes/templates/$1.mustache'
.not( '#ca-watch, #ca-unwatch' ).addClass( 'collapsible' );
},


// Automatically clear mock calls and instances between every test
$.fn.collapsibleTabs = function ( options ) {
clearMocks: true,
// Merge options into the defaults
const settings = $.extend( {}, $.collapsibleTabs.defaults, options );


// Indicates whether the coverage information should be collected while executing the test
// return if the function is called on an empty jquery object
collectCoverage: true,
if ( !this.length ) {
return this;
}


// An array of glob patterns indicating a set of files fo
this.each( function () {
// which coverage information should be collected
const $el = $( this );
collectCoverageFrom: [
// add the element to our array of collapsible managers
'resources/**/*.(js|vue)'
$.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 ) );
} );
} );


// The directory where Jest should output its coverage files
// if we haven't already bound our resize handler, bind it now
coverageDirectory: 'coverage',
if ( !boundEvent ) {
boundEvent = true;
$( window ).on( 'resize', mw.util.debounce( function () {
rAF( $.collapsibleTabs.handleResize );
}, 10 ) );
}


// An array of regexp pattern strings used to skip coverage collection
// call our resize handler to setup the page
coveragePathIgnorePatterns: [
rAF( $.collapsibleTabs.handleResize );
'/node_modules/',
// When adding new links, a resize should be triggered (T139830).
'/resources/skins.vector.typographySurvey/'
mw.hook( 'util.addPortletLink' ).add( $.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 ) {
const settings = $collapsible.parent().data( 'collapsibleTabsSettings' );
if ( settings ) {
$collapsible.data( 'collapsibleTabsSettings', {
expandedContainer: settings.expandedContainer,
collapsedContainer: settings.collapsedContainer,
expandedWidth: $collapsible.outerWidth( true )
} );
}
},
getSettings: function ( $collapsible ) {
let settings = $collapsible.data( 'collapsibleTabsSettings' );
if ( !settings ) {
$.collapsibleTabs.addData( $collapsible );
settings = $collapsible.data( 'collapsibleTabsSettings' );
}
// it's possible for getSettings to return undefined
// if no data attributes have been set
// see T177108#6310908.
// In particular, a gadget may have added a collapsible link to the list:
// e.g.
// $('<li class="collapsible">my link</a>').appendTo( $('#p-cactions ul') )
return settings || {};
},
handleResize: function () {
$.collapsibleTabs.instances.forEach( function ( $el ) {
const data = $.collapsibleTabs.getSettings( $el );


// An object that configures minimum threshold enforcement for coverage results
if ( $.isEmptyObject( data ) || data.shifting ) {
coverageThreshold: {
return;
global: {
}
branches: 31,

functions: 39,
// if the two navigations are colliding
lines: 38,
if ( $el.children( data.collapsible ).length && data.collapseCondition() ) {
statements: 38
/**
* 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() );
}

const $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 ) {
const outerData = $.collapsibleTabs.getSettings( $moving );
if ( !outerData ) {
return;
}
const 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
const 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 ) {
const data = $.collapsibleTabs.getSettings( $moving );
if ( !data ) {
return;
}
const expandedContainerSettings =
$.collapsibleTabs.getSettings( $( data.expandedContainer ) );
if ( !expandedContainerSettings ) {
return;
}
expandedContainerSettings.shifting = true;

// grab the next appearing placeholder so we can use it for replacing
const $target = $( data.expandedContainer ).find( 'span.placeholder' ).first();
const 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.outerWidth( true ) || 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 () {
let 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;
}
}
};
},
}


// An array of file extensions your modules use
module.exports = Object.freeze( { init: init } );
moduleFileExtensions: [
'js',
'json',
'vue'
],


// The paths to modules that run some code to configure or
/* eslint-disable no-jquery/no-jquery-constructor, no-jquery/no-other-methods, no-jquery/no-css, no-jquery/no-find-collection,
// set up the testing environment before each test
no-jquery/no-each-collection, no-jquery/no-attr */
setupFiles: [
/**
'./jest.setup.js'
* Collapsible tabs for Vector
*/
],
function init() {
const cactionsId = 'p-cactions',
$cactions = $( '#' + cactionsId ),
// eslint-disable-next-line no-jquery/no-global-selector
$tabContainer = $( '#p-views ul' );
let initialCactionsWidth = function () {
// HACK: This depends on a discouraged feature of jQuery width().
// The #p-cactions element is generally hidden by default, but
// the consumers of this function need to know the width that the
// "More" menu would consume if it were visible. This means it
// must not return 0 if hidden, but rather virtually render it
// and compute its width, then hide it again. jQuery width() does
// all that for us.
const width = $cactions.width() || 0;
initialCactionsWidth = function () {
return width;
};
return width;
};


testEnvironment: 'jsdom',
// 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 () {
let expandedWidth;
// If the dropdown was hidden, show it
if ( !mw.util.isPortletVisible( cactionsId ) ) {
mw.util.showPortlet( cactionsId );
// Now that it is visible, force-render it virtually
// to get its expanded width, then shrink it 1px before we
// yield from JS (which means the expansion won't be visible).
// Then animate from the 1px to the expanded width.
expandedWidth = $cactions.width();
// eslint-disable-next-line no-jquery/no-animate
$cactions
.css( 'width', '1px' )
.animate( { width: expandedWidth }, '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.animate( { width: '1px' }, 'normal', function () {
$( this ).attr( 'style', '' );
mw.util.hidePortlet( cactionsId );
} );
}
} )
.collapsibleTabs( {
expandCondition: function ( eleWidth ) {
// This looks a bit awkward because we're doing expensive queries as late
// as possible.
const 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 () {
let collapsibleWidth = 0,
doCollapse = false;


transform: {
// This looks a bit awkward because we're doing expensive queries as late
'^.+\\.mustache?$': 'mustache-jest',
// as possible.
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest'
// 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 ( mw.util.isPortletVisible( cactionsId ) ) {
return true;
}

// If we reach here, this means:
// 1. #p-cactions is currently empty and invisible (e.g. when logged out),
// 2. and, there is at least one li.collapsible link in #p-views, as asserted
// by handleResize() before calling here. Such link exists e.g. as
// "View history" on articles, but generally not on special pages.
// 3. and, the left-navigation and right-navigation are overlapping
// each other, e.g. when making the window very narrow, or if a gadget
// added a lot of tabs.
$tabContainer.children( 'li.collapsible' ).each( function ( _index, element ) {
collapsibleWidth += $( element ).width() || 0;
if ( collapsibleWidth > initialCactionsWidth() ) {
// We've found one or more collapsible links that are wider
// than the "More" menu would be if it were made visible,
// which means it is worth doing a collapsing.
doCollapse = true;
// Stop this possibly expensive loop the moment the condition is met once.
return false;
}
return;
} );
return doCollapse;
}
} );
}

module.exports = Object.freeze( { init: init } );
/**
* An object containing the data to help create a portlet.
*
* @typedef {Object} Hint
* @property {string} type
*/

/**
* Creates default portlet.
*
* @param {Element} portlet
* @return {Element}
*/
function addDefaultPortlet( portlet ) {
const ul = portlet.querySelector( 'ul' );
if ( !ul ) {
return portlet;
}
}
ul.classList.add( 'vector-menu-content-list' );
const label = portlet.querySelector( 'label' );
if ( label ) {
const labelDiv = document.createElement( 'div' );
labelDiv.classList.add( 'vector-menu-heading' );
labelDiv.innerHTML = label.innerText;
portlet.insertBefore( labelDiv, label );
label.remove();
}
let wrapper = portlet.querySelector( 'div:last-child' );
if ( wrapper ) {
ul.remove();
wrapper.appendChild( ul );
wrapper.classList.add( 'vector-menu-content' );
} else {
wrapper = document.createElement( 'div' );
wrapper.classList.add( 'vector-menu-content' );
ul.remove();
wrapper.appendChild( ul );
portlet.appendChild( wrapper );
}
portlet.classList.add( 'vector-menu', 'vector-menu-portal' );

return portlet;
}

/**
* A hook handler for util.addPortlet hook.
* It creates a portlet based on the hint, and adabt it to vector skin.
*
* @param {Element} portlet
* @return {Element}
*/
function addPortletHandler( portlet ) {
portlet.classList.remove( 'mw-portlet-js' );
return addDefaultPortlet( portlet );
}

/**
*
* @return {{addPortletHandler: (function(Element): Element)}}
*/
function main() {
mw.hook( 'util.addPortlet' ).add( addPortletHandler );
// Update any portlets that were created prior to the hook being registered.
document.querySelectorAll( '.mw-portlet-js' ).forEach( addPortletHandler );
return {
addPortletHandler
};
}

module.exports = {
main, addPortletHandler
};
};
/* eslint-disable no-jquery/no-jquery-constructor */
/** @interface MediaWikiPageReadyModule */
const
collapsibleTabs = require( './collapsibleTabs.js' ),
/** @type {MediaWikiPageReadyModule} */
pageReady = require( /** @type {string} */( 'mediawiki.page.ready' ) ),
portlets = require( './portlets.js' ),
vector = require( './vector.js' ),
teleportTarget = /** @type {HTMLElement} */require( /** @type {string} */ ( 'mediawiki.page.ready' ) ).teleportTarget;

function main() {
collapsibleTabs.init();
$( vector.init );
portlets.main();
pageReady.loadSearchModule( 'mediawiki.searchSuggest' );
teleportTarget.classList.add( 'vector-body' );
}

main();

Latest revision as of 01:50, 8 February 2024

'use strict';

// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
	moduleNameMapper: {
		'@wikimedia/codex-search': '@wikimedia/codex',
		'^./templates/(.*).mustache': '<rootDir>/includes/templates/$1.mustache'
	},

	// Automatically clear mock calls and instances between every test
	clearMocks: true,

	// Indicates whether the coverage information should be collected while executing the test
	collectCoverage: true,

	// An array of glob patterns indicating a set of files fo
	//  which coverage information should be collected
	collectCoverageFrom: [
		'resources/**/*.(js|vue)'
	],

	// The directory where Jest should output its coverage files
	coverageDirectory: 'coverage',

	// An array of regexp pattern strings used to skip coverage collection
	coveragePathIgnorePatterns: [
		'/node_modules/',
		'/resources/skins.vector.typographySurvey/'
	],

	// An object that configures minimum threshold enforcement for coverage results
	coverageThreshold: {
		global: {
			branches: 31,
			functions: 39,
			lines: 38,
			statements: 38
		}
	},

	// An array of file extensions your modules use
	moduleFileExtensions: [
		'js',
		'json',
		'vue'
	],

	// The paths to modules that run some code to configure or
	// set up the testing environment before each test
	setupFiles: [
		'./jest.setup.js'
	],

	testEnvironment: 'jsdom',

	transform: {
		'^.+\\.mustache?$': 'mustache-jest',
		'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest'
	}
};