User:Guycn2/sandbox4.js
Appearance
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)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
== Multi Patrol ==
Use this tool to mark multiple revisions as patrolled at the
touch of a button, directly from the revision history page.
See also:
* [[MediaWiki:סקריפטים/113.css]] – for the corresponding style sheet
* [[MediaWiki:סקריפטים/113.css/LTR.css]] – for the corresponding style sheet, adapted for left-to-right interface
* [[MediaWiki:סקריפטים/113.css/RTL.css]] – for the corresponding style sheet, adapted for right-to-left interface
Skins supported:
Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.
Also fully supported on the mobile interface.
Dependencies:
* mediawiki.api
* mediawiki.util
* oojs-ui-core
Written by: [[User:Guycn2]]
__________________________________________________
== סקריפט לסימון מרובה של גרסאות כבדוקות ==
כלי זה מאפשר לסמן גרסאות מרובות כבדוקות בלחיצת כפתור,
ישירות מתוך דף הגרסאות הקודמות.
ראו גם:
* [[מדיה ויקי:סקריפטים/113.css]] – לגיליון הסגנונות המשויך
* [[מדיה ויקי:סקריפטים/113.css/LTR.css]] – לגיליון הסגנונות המשויך, מותאם לממשק שמוצג משמאל לימין
* [[מדיה ויקי:סקריפטים/113.css/RTL.css]] – לגיליון הסגנונות המשויך, מותאם לממשק שמוצג מימין לשמאל
עיצובים נתמכים:
וקטור (2022 ו־2010), מונובוק, מחוץ לזמן, מינרווה נויה.
הסקריפט נתמך במלואו גם בממשק למכשירים ניידים.
נכתב ע"י: [[משתמש:Guycn2]]
*/
/**
* @file MultiPatrol - A MediaWiki user script that enables batch patrolling of
* multiple revisions directly from the page history interface. Users can
* select multiple revisions using checkboxes and mark them all as patrolled
* with a single button click, with real-time progress feedback.
*
* @author Guycn2
* @version 1.0
* @license CC-BY-SA-4.0
* @requires mediawiki.api
* @requires mediawiki.util
* @requires oojs-ui-core
*/
( () => {
'use strict';
if (
mw.config.get( 'multiPatrolLoaded' ) ||
mw.config.get( 'wgAction' ) !== 'history' ||
!mw.config.get( 'wgArticleId' )
)
return;
mw.config.set( 'multiPatrolLoaded', true );
/**
* Main class for the MultiPatrol functionality. Handles the user interface,
* API calls for patrolling revisions, and progress tracking. Supports both
* English and Hebrew localization.
*
* @class MultiPatrol
*/
class MultiPatrol {
#messages;
#lang;
#$elements;
#alreadyRun;
#api;
#progressBar;
#panel;
/**
* Creates a new MultiPatrol instance. Initializes localization messages for
* English and Hebrew, sets the current user language, and prepares internal
* state properties for tracking UI elements and run status.
*
* @function constructor
*/
constructor() {
this.#messages = {
en: {
btnText: 'Mark selected revisions as patrolled',
noRevsSelected: 'No revisions were selected.',
statusRunning: 'Script is running',
statusCount: 'revision $1 of $2',
finishedRunning: 'Script has finished running',
finishedTime: '$1 seconds',
successCount: '$1 revisions were marked as patrolled (this may include revisions that had already been patrolled before).',
failureCount: '$1 revisions could not be marked as patrolled. They may be too old (more than 30 days).',
viewLog: 'View the patrol log for this page »'
},
he: {
btnText: 'סימון הגרסאות שנבחרו כבדוקות',
noRevsSelected: 'לא נבחרו גרסאות.',
statusRunning: 'הסקריפט בפעולה',
statusCount: 'גרסה $1 מתוך $2',
finishedRunning: 'פעולת הסקריפט הושלמה',
finishedTime: '$1 שניות',
successCount: '$1 גרסאות סומנו כבדוקות (ייתכן שזה כולל גרסאות שכבר היו מסומנות כבדוקות לפני כן).',
failureCount: 'לא ניתן היה לסמן $1 גרסאות כבדוקות. ייתכן שהן ישנות מדי (מעל 30 יום).',
viewLog: 'לצפייה ביומן השינויים הבדוקים של דף זה »'
}
}; // messages END
this.#lang = mw.config.get( 'wgUserLanguage' );
this.#$elements = {};
this.#alreadyRun = false;
this.#api = null;
this.#progressBar = null;
this.#panel = null;
} // constructor END
/**
* Retrieves a localized message string based on the user's interface language.
* Falls back to English if the message is not available in the user's language,
* or returns the key itself if no translation exists.
*
* @function #i18n
* @private
* @param {string} key - The message key to look up in the messages object.
* @returns {string} The localized message string, English fallback, or the
* original key if no translation is found.
*/
#i18n( key ) {
return this.#messages[ this.#lang ]?.[ key ] || this.#messages.en[ key ] || key;
}
/**
* Loads the required CSS stylesheets for the MultiPatrol interface. Fetches
* the base stylesheet and a direction-specific stylesheet (RTL or LTR) based
* on the current document direction, both from Hebrew Wikipedia.
*
* @function loadCSS
* @public
*/
loadCSS() {
mw.loader.load(
'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/113.css&action=raw&ctype=text/css',
'text/css'
);
const dir = document.documentElement.dir === 'rtl' ? 'RTL' : 'LTR';
mw.loader.load(
`https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/113.css/${ dir }.css&action=raw&ctype=text/css`,
'text/css'
);
}
/**
* Sets up the multi-patrol button in the page history interface. Attaches a
* click event listener to the buttons area that creates and inserts the patrol
* button when the advanced options are shown. The button triggers the batch
* patrolling process when clicked.
*
* @function setupMultiPatrolBtn
* @public
*/
setupMultiPatrolBtn() {
this.#$elements.$buttonsArea = $( '.mw-history-compareselectedversions' );
const $compareBtn =
this.#$elements.$buttonsArea.find( '.mw-history-compareselectedversions-button' );
if ( !$compareBtn.length )
return;
this.#$elements.$buttonsArea.on( 'click', evt => {
// The 'history-show-advanced-btn' element is specific to hewiki,
// and is created locally by [[MediaWiki:FixHistPage.js]].
// In order for this script to work in other wikis,
// $multiPatrolBtn must be added to the page independently
// of the hewiki-specific 'history-show-advanced-btn' element!
if ( evt.target.classList.contains( 'history-show-advanced-btn' ) ) {
this.#$elements.$multiPatrolBtn = $( '<button>' )
.addClass( [ 'cdx-button', 'history-multi-patrol-btn' ] )
.attr( 'type', 'button' )
.text( this.#i18n( 'btnText' ) )
.on( 'click', () => this.#patrolSelectedRevs() )
.insertAfter( $compareBtn );
}
} );
} // setupMultiPatrolBtn END
/**
* Patrols all currently selected revisions in the page history. Collects the
* revision IDs from checked checkboxes, sends patrol API requests sequentially
* with a delay between each, updates the progress bar in real time, and
* displays success/failure statistics upon completion.
*
* @function #patrolSelectedRevs
* @private
* @async
* @returns {Promise<void>} Resolves when all selected revisions have been
* processed and the results have been displayed.
*/
async #patrolSelectedRevs() {
if ( !this.#alreadyRun ) {
this.#alreadyRun = true;
await this.#prepareForFirstRun();
}
const idsToPatrol = [];
this.#$elements.$revElements.each( ( index, element ) => {
const $element = $( element );
if ( $element.prop( 'checked' ) ) {
const match = $element.attr( 'name' ).match( /ids\[(\d+)\]/ );
if ( match?.[ 1 ] )
idsToPatrol.unshift( parseInt( match[ 1 ], 10 ) );
}
} );
if ( !idsToPatrol.length ) {
OO.ui.alert( this.#i18n( 'noRevsSelected' ) );
return;
}
this.#$elements.$statusCountTotal.text( idsToPatrol.length );
this.#prepareForRun();
let successCount = 0, failureCount = 0;
const startTime = mw.now();
for ( const [ index, id ] of idsToPatrol.entries() ) {
this.#$elements.$statusCountCurr.text( index + 1 );
try {
const params = { action: 'patrol', revid: id };
const data = await this.#api.postWithToken( 'patrol', params );
if ( data?.patrol )
successCount++;
else
throw 'unknown_error';
} catch {
failureCount++;
}
this.#progressBar.setProgress( ( index + 1 ) / idsToPatrol.length * 100 );
if ( index < idsToPatrol.length - 1 )
await this.#sleep();
} // for loop END
const finishTime = mw.now();
this.#$elements.$finishedTimeSecs.text(
( ( finishTime - startTime ) / 1000 ).toFixed( 1 )
);
this.#postRun( successCount, failureCount );
} // patrolSelectedRevs END
/**
* Performs one-time initialization before the first patrol run. Loads required
* MediaWiki modules, creates all UI elements including the status display,
* progress bar, and statistics panel, and initializes the MediaWiki API
* instance for making patrol requests.
*
* @function #prepareForFirstRun
* @private
* @async
* @returns {Promise<void>} Resolves when all modules are loaded and UI
* elements are created and attached to the DOM.
*/
async #prepareForFirstRun() {
await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util', 'oojs-ui-core' ] );
const $pageHistory = $( '#pagehistory' );
this.#$elements.$revElements =
$pageHistory.find(
'.mw-contributions-list input[ form="mw-history-revisionactions" ][ type="checkbox" ]'
);
this.#$elements.$toggleControls =
this.#$elements.$buttonsArea.find( '.mw-checkbox-toggle-controls' );
const $statusCount = $( '<span>' )
.attr( 'id', 'multi-patrol-status-count' )
.html(
this.#i18n( 'statusCount' )
.replace( '$1', '<span id="multi-patrol-status-count-curr"></span>' )
.replace( '$2', '<span id="multi-patrol-status-count-total"></span>' )
);
this.#$elements.$statusCountCurr =
$statusCount.children( '#multi-patrol-status-count-curr' );
this.#$elements.$statusCountTotal =
$statusCount.children( '#multi-patrol-status-count-total' );
this.#$elements.$status = $( '<p>' )
.attr( 'id', 'multi-patrol-status' )
.text( this.#i18n( 'statusRunning' ) )
.append( $statusCount );
const $finishedTime = $( '<span>' )
.attr( 'id', 'multi-patrol-finished-time' )
.html(
this.#i18n( 'finishedTime' )
.replace( '$1', '<span id="multi-patrol-finished-time-secs"></span>' )
);
this.#$elements.$finishedTimeSecs =
$finishedTime.children( '#multi-patrol-finished-time-secs' );
this.#$elements.$finished = $( '<p>' )
.attr( 'id', 'multi-patrol-finished' )
.text( this.#i18n( 'finishedRunning' ) )
.append( $finishedTime );
this.#progressBar = new OO.ui.ProgressBarWidget( {
id: 'multi-patrol-progress-bar',
progress: 0
} );
this.#$elements.$successCount = $( '<p>' )
.html(
this.#i18n( 'successCount' )
.replace( '$1', '<span id="multi-patrol-success-count-num"></span>' )
);
this.#$elements.$successCountNum =
this.#$elements.$successCount.children( '#multi-patrol-success-count-num' );
this.#$elements.$failureCount = $( '<p>' )
.html(
this.#i18n( 'failureCount' )
.replace( '$1', '<span id="multi-patrol-failure-count-num"></span>' )
);
this.#$elements.$failureCountNum =
this.#$elements.$failureCount.children( '#multi-patrol-failure-count-num' );
const logParams = { type: 'patrol', page: mw.config.get( 'wgPageName' ) };
const logUrl = mw.util.getUrl( 'Special:Log', logParams );
const $logLink = $( '<a>' ).attr( 'href', logUrl ).text( this.#i18n( 'viewLog' ) );
const $logLinkContainer = $( '<p>' ).append( $logLink );
this.#$elements.$stats = $( '<div>' )
.attr( 'id', 'multi-patrol-stats' )
.append(
this.#$elements.$successCount,
this.#$elements.$failureCount,
$logLinkContainer
);
const $panelContent = $( '<div>' )
.attr( 'id', 'multi-patrol-panel-content' )
.append(
this.#$elements.$status,
this.#$elements.$finished,
this.#progressBar.$element,
this.#$elements.$stats
);
this.#panel = new OO.ui.PanelLayout( {
expanded: false,
framed: true,
padded: true,
$content: $panelContent
} ).toggle( false );
$pageHistory.before( this.#panel.$element );
this.#api = new mw.Api( { userAgent: 'MultiPatrol/1.0' } );
} // prepareForFirstRun END
/**
* Prepares the UI state before each patrol run. Disables the patrol button
* and revision checkboxes to prevent concurrent runs, resets the progress bar,
* shows the panel with status information, and hides any previous results.
*
* @function #prepareForRun
* @private
*/
#prepareForRun() {
[ 'multiPatrolBtn', 'revElements' ]
.forEach( item => this.#$elements[ `$${ item }` ].attr( 'disabled', true ) );
this.#$elements.$toggleControls.css( 'pointer-events', 'none' );
this.#progressBar.pushPending().setProgress( 0 );
this.#panel.toggle( true ).scrollElementIntoView();
this.#$elements.$status.show();
[ 'finished', 'successCount', 'failureCount', 'stats' ]
.forEach( item => this.#$elements[ `$${ item }` ].hide() );
}
/**
* Handles the UI updates after a patrol run completes. Hides the running
* status, displays completion message and elapsed time, shows success and
* failure counts if applicable, and re-enables the patrol button and
* revision checkboxes for subsequent runs.
*
* @function #postRun
* @private
* @param {number} successCount - The number of revisions that were
* successfully marked as patrolled.
* @param {number} failureCount - The number of revisions that could not be
* marked as patrolled, typically due to age restrictions.
*/
#postRun( successCount, failureCount ) {
this.#$elements.$status.hide();
this.#$elements.$finished.show();
if ( successCount ) {
this.#$elements.$successCountNum.text( successCount );
this.#$elements.$successCount.show();
}
if ( failureCount ) {
this.#$elements.$failureCountNum.text( failureCount );
this.#$elements.$failureCount.show();
}
this.#progressBar.popPending();
this.#$elements.$stats.fadeIn();
[ 'multiPatrolBtn', 'revElements' ]
.forEach( item => this.#$elements[ `$${ item }` ].attr( 'disabled', false ) );
this.#$elements.$toggleControls.css( 'pointer-events', 'auto' );
} // postRun END
/**
* Creates a promise that resolves after a 200 millisecond delay. Used to
* add a pause between sequential API patrol requests to avoid overwhelming
* the server with rapid-fire requests.
*
* @function #sleep
* @private
* @returns {Promise<void>} A promise that resolves after the delay period.
*/
#sleep() {
return new Promise( resolve => setTimeout( resolve, 200 ) );
}
} // MultiPatrol class definition END
const multiPatrol = new MultiPatrol();
multiPatrol.loadCSS();
$( () => multiPatrol.setupMultiPatrolBtn() );
} )();