User:MM abc.xyz/common.js: Difference between revisions
Content deleted Content added
MM abc.xyz (talk | contribs) Created page with "→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.: /..." Tags: Mobile edit Mobile web edit |
MM abc.xyz (talk | contribs) 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'
}
};