User:MM abc.xyz/common.js

From Wikimedia Foundation Governance Wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* 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.
 */

/** @interface CollapsibleTabsOptions */
function init() {
	/** @type {boolean|undefined} */ let boundEvent;
	const isRTL = document.documentElement.dir === 'rtl';
	const rAF = window.requestAnimationFrame || setTimeout;

	// Mark the tabs which can be collapsed under the more menu
	// eslint-disable-next-line no-jquery/no-global-selector
	$( '#p-views li' )
		.not( '#ca-watch, #ca-unwatch' ).addClass( 'collapsible' );

	$.fn.collapsibleTabs = function ( options ) {
		// Merge options into the defaults
		const settings = $.extend( {}, $.collapsibleTabs.defaults, options );

		// return if the function is called on an empty jquery object
		if ( !this.length ) {
			return this;
		}

		this.each( function () {
			const $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( function () {
				rAF( $.collapsibleTabs.handleResize );
			}, 10 ) );
		}

		// call our resize handler to setup the page
		rAF( $.collapsibleTabs.handleResize );
		// When adding new links, a resize should be triggered (T139830).
		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 );

				if ( $.isEmptyObject( data ) || 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() );
				}

				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;
		}
	};
}

module.exports = Object.freeze( { init: init } );

/* eslint-disable no-jquery/no-jquery-constructor, no-jquery/no-other-methods, no-jquery/no-css, no-jquery/no-find-collection,
	no-jquery/no-each-collection, no-jquery/no-attr */
/**
 * 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;
	};

	// 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;

				// 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 ( 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();