MediaWiki:Common.js : Différence entre versions
| Ligne 34 : | Ligne 34 : | ||
}); | }); | ||
}); | }); | ||
| + | |||
| + | * CatRename | ||
| + | * | ||
| + | * Ajoute un onglet permettant de renommer une catégorie, en déplaçant les pages | ||
| + | * incluses dans celle-ci. Permet de faire faire l'action à un bot en un clic. | ||
| + | * | ||
| + | * {{Projet:JavaScript/Script|CatRename}} | ||
| + | */ | ||
| + | /* <nowiki> */ | ||
| + | |||
| + | /* globals mw, OO, $ */ | ||
| + | |||
| + | if ( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { | ||
| + | mw.loader.using( 'mediawiki.util', function () { | ||
| + | 'use strict'; | ||
| + | |||
| + | // Site-related parameters | ||
| + | const TAG = 'RenommageCategorie'; | ||
| + | const DAILY_LIMIT = 250; | ||
| + | const RBOT_PAGE = 'Wikipédia:Bot/Requêtes/Catégories'; | ||
| + | const DR_TEMPLATE = '{{Suppression Immédiate|raison=Catégorie récemment renommée en [[:Catégorie:$2]] ($3)|utilisateur=$4}}\n\n'; | ||
| + | const RBOT_TEMPLATE = '\n{{Déplacement catégorie|ancienne=$1|nouvelle=$2|raison=$3|demandeur=$4}}'; | ||
| + | |||
| + | // Literal non-breaking space, for situations where HTML entities can't be used | ||
| + | const NBSP = String.fromCharCode( 0xA0 ); | ||
| + | |||
| + | // Messages | ||
| + | const messages = { | ||
| + | 'fr': { | ||
| + | 'catrename-title': 'Renommer une catégorie', | ||
| + | 'catrename-portlet-title': 'CatRename', | ||
| + | |||
| + | 'catrename-action-rename': 'Renommer', | ||
| + | 'catrename-action-cancel': 'Annuler', | ||
| + | 'catrename-action-rbot': '… ou faire faire la tâche par un bot', | ||
| + | |||
| + | 'catrename-checkbox-movetalk': 'Renommer aussi la page de discussion associée', | ||
| + | 'catrename-checkbox-leave-redirect': 'Laisser une redirection vers le nouveau titre', | ||
| + | 'catrename-checkbox-post-dr': 'Déposer une demande de suppression de l\'ancienne catégorie', | ||
| + | 'catrename-checkbox-watch': 'Suivre les catégories originale et nouvelle', | ||
| + | 'catrename-checkbox-watch-members': 'Suivre les pages modifiées', | ||
| + | |||
| + | 'catrename-field-title': 'Nouveau titre' + NBSP + ':', | ||
| + | 'catrename-field-reason': 'Motif' + NBSP + ':', | ||
| + | |||
| + | 'catrename-summary': 'Remplacement de la catégorie [[Catégorie:$1]] par [[Catégorie:$2]] : $3', | ||
| + | 'catrename-dr-summary': 'Demande de suppression après renommage', | ||
| + | 'catrename-rbot-summary': 'RBOT : Demande de renommage de catégorie', | ||
| + | |||
| + | 'catrename-status-checkcategory': 'Vérification de la catégorie cible', | ||
| + | 'catrename-status-getmembers': 'Récupération des pages membres de la catégorie', | ||
| + | 'catrename-status-waitinglock': 'En attente de la fin de renommage dans d\'autres onglets', | ||
| + | 'catrename-status-checklimits': 'Vérification de la limite journalière', | ||
| + | 'catrename-status-editmembers': 'Modification de la page $1 sur $2', | ||
| + | 'catrename-status-renamecategory': 'Renommage de la catégorie', | ||
| + | 'catrename-status-postdr': 'Dépôt de la demande de suppression', | ||
| + | 'catrename-status-postrbot': 'Dépôt de la requête aux bots', | ||
| + | |||
| + | 'catrename-error-canceled': 'Le processus de renommage a été annulé.', | ||
| + | 'catrename-error-same': 'Le nouveau titre est identique au titre actuel.', | ||
| + | 'catrename-error-invalidtitle': 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.', | ||
| + | 'catrename-error-noreason': 'Veuillez indiquer un motif pour ce renommage.', | ||
| + | 'catrename-error-protected': 'Cette catégorie est protégée, vous n\'êtes pas autorisé à la renommer.', | ||
| + | 'catrename-error-categoryexist': 'Il existe déjà une catégorie avec ce nom…', | ||
| + | 'catrename-error-limitreached': 'Le renommage de cette catégorie vous ferait faire plus de $1 modifications avec ce script en moins de 24h. Vous pouvez cependant faire une requête aux bots via le bouton en bas à gauche.', | ||
| + | 'catrename-error-categorypresent': 'La page contient déjà la nouvelle catégorie.', | ||
| + | 'catrename-error-notfound': 'La catégorie n\'a pas été trouvée dans le code de la page, peut-être est-elle incluse via un modèle' + NBSP + '?', | ||
| + | 'catrename-error-pageprotected': 'La page est protégée en écriture.', | ||
| + | 'catrename-error-articleexists': 'Impossible de déplacer la catégorie, la page de destination «' + NBSP + '$1' + NBSP + '» existe déjà.', | ||
| + | } | ||
| + | }; | ||
| + | mw.messages.set( messages.fr ); | ||
| + | var lang = mw.config.get( 'wgUserLanguage' ); | ||
| + | if ( lang !== 'fr' && lang in messages ) { | ||
| + | mw.messages.set( messages[ lang ] ); | ||
| + | } | ||
| + | |||
| + | |||
| + | var isBootstrapped = false; | ||
| + | var instanceWindowManager; | ||
| + | var instanceCatRename; | ||
| + | |||
| + | $( function ( $ ) { | ||
| + | var portlet = mw.util.addPortletLink( 'p-cactions', '#', mw.msg( 'catrename-portlet-title' ) ); | ||
| + | $( portlet ).on( 'click', function ( e ) { | ||
| + | e.preventDefault(); | ||
| + | mw.loader.using( [ 'oojs-ui', 'mediawiki.storage', 'mediawiki.api' ], function () { | ||
| + | bootstrapOnce(); | ||
| + | instanceCatRename.open(); | ||
| + | } ); | ||
| + | } ); | ||
| + | } ); | ||
| + | |||
| + | |||
| + | /* Instanciate CatRename and add it to MediaWiki's UI. */ | ||
| + | |||
| + | function bootstrapOnce() { | ||
| + | if (isBootstrapped) { | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | isBootstrapped = true; | ||
| + | |||
| + | /** | ||
| + | * Main class of the gadget CatRename, which is displayed as a ProcessDialog | ||
| + | * | ||
| + | * @class | ||
| + | * @extends OO.ui.ProcessDialog | ||
| + | * | ||
| + | * @constructor | ||
| + | */ | ||
| + | var CatRename = function () { | ||
| + | // Initialize config | ||
| + | var config = { size: 'medium' }; | ||
| + | |||
| + | // Parent constructor | ||
| + | CatRename.parent.call( this, config ); | ||
| + | |||
| + | // Properties | ||
| + | this.api = new mw.Api( { timeout: 7000 } ); | ||
| + | this.oldTitle; | ||
| + | this.newTitle; | ||
| + | this.oldPageName; | ||
| + | this.newPageName; | ||
| + | this.reason; | ||
| + | this.deferred; | ||
| + | this.members; | ||
| + | this.lockID; | ||
| + | this.nextTab; | ||
| + | this.noSpammingDelay; | ||
| + | |||
| + | // Graphical properties | ||
| + | this.configContent = new OO.ui.PanelLayout( { padded: true, expanded: false } ); | ||
| + | this.statusContent = new OO.ui.PanelLayout( { padded: true, expanded: false } ); | ||
| + | this.newNameInput; | ||
| + | this.reasonInput; | ||
| + | this.optionCheckboxes; | ||
| + | this.layout; | ||
| + | this.$body; | ||
| + | this.statusIndicator; | ||
| + | this.pagesInError; | ||
| + | }; | ||
| + | |||
| + | |||
| + | |||
| + | /* Setup */ | ||
| + | |||
| + | OO.inheritClass( CatRename, OO.ui.ProcessDialog ); | ||
| + | |||
| + | |||
| + | |||
| + | /* Static Properties */ | ||
| + | |||
| + | CatRename.static.name = 'catrename'; | ||
| + | CatRename.static.title = mw.msg( 'catrename-title' ); | ||
| + | CatRename.static.actions = [ | ||
| + | { action: 'rename', label: mw.msg( 'catrename-action-rename' ), flags: [ 'primary', 'progressive' ] }, | ||
| + | { action: 'cancel', label: mw.msg( 'catrename-action-cancel' ), flags: [ 'safe', 'back' ] }, | ||
| + | { action: 'rbot', label: mw.msg( 'catrename-action-rbot' ), flags: 'other' } | ||
| + | ]; | ||
| + | |||
| + | |||
| + | |||
| + | /* ProcessDialog-related Methods */ | ||
| + | |||
| + | /** | ||
| + | * Build the interface displayed inside the ProcessDialog box. | ||
| + | */ | ||
| + | CatRename.prototype.initialize = function () { | ||
| + | CatRename.parent.prototype.initialize.apply( this, arguments ); | ||
| + | |||
| + | this.newNameInput = new OO.ui.TextInputWidget( { value: mw.config.get( 'wgTitle' ) } ); | ||
| + | this.reasonInput = new OO.ui.TextInputWidget( { | ||
| + | maxLength: 500, | ||
| + | name: 'wpSummary' | ||
| + | } ); | ||
| + | |||
| + | this.optionCheckboxes = new OO.ui.CheckboxMultiselectInputWidget( { | ||
| + | value: [ 'movetalk', 'post-dr' ], | ||
| + | options: [ | ||
| + | { data: 'movetalk', label: mw.msg( 'catrename-checkbox-movetalk' ) }, | ||
| + | ( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ? | ||
| + | { data: 'leave-redirect', label: mw.msg( 'catrename-checkbox-leave-redirect' ) } : | ||
| + | { data: 'post-dr', label: mw.msg( 'catrename-checkbox-post-dr' ) } | ||
| + | ), | ||
| + | { data: 'watch', label: mw.msg( 'catrename-checkbox-watch' ) }, | ||
| + | { data: 'watch-members', label: mw.msg( 'catrename-checkbox-watch-members' ) } | ||
| + | ] | ||
| + | } ); | ||
| + | |||
| + | this.layout = new OO.ui.Widget( { | ||
| + | content: [ | ||
| + | new OO.ui.FieldLayout( | ||
| + | this.newNameInput, { | ||
| + | align: 'top', | ||
| + | label: mw.msg( 'catrename-field-title' ), | ||
| + | } | ||
| + | ), | ||
| + | new OO.ui.FieldLayout( | ||
| + | this.reasonInput, { | ||
| + | align: 'top', | ||
| + | label: mw.msg( 'catrename-field-reason' ), | ||
| + | } | ||
| + | ), | ||
| + | new OO.ui.FieldLayout( | ||
| + | this.optionCheckboxes, {} | ||
| + | ) | ||
| + | ], | ||
| + | } ); | ||
| + | |||
| + | this.configContent.$element.append( this.layout.$element ); | ||
| + | |||
| + | this.$body.append( this.configContent.$element ); | ||
| + | |||
| + | this.statusIndicator = $( '<h3>' ) | ||
| + | .css( 'text-align', 'center' ) | ||
| + | .css( 'margin-top', '1em' ) | ||
| + | .css( 'margin-bottom', '2em' ); | ||
| + | this.pagesInError = $( '<ul>' ); | ||
| + | this.statusContent.$element.append( this.statusIndicator ).append( this.pagesInError ); | ||
| + | |||
| + | this.setSize( this.size ); | ||
| + | this.updateSize(); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Get a process for taking action. | ||
| + | * | ||
| + | * This method is called within the ProcessDialog when the user clicks | ||
| + | * on an action button (the one defined in CatRename.static.actions). | ||
| + | * Here is defined in which order each method of the category moving | ||
| + | * process is called. | ||
| + | * @param {string} action Name of the action button clicked. | ||
| + | * @return {OO.ui.Process} Action process. | ||
| + | */ | ||
| + | CatRename.prototype.getActionProcess = function ( action ) { | ||
| + | var process = new OO.ui.Process(), | ||
| + | options = this.optionCheckboxes.getValue(); | ||
| + | |||
| + | if ( action === 'cancel' || action === '' ) { // empty string when closing with Escape key | ||
| + | return process.next( this.unlockMultitabs, this ) | ||
| + | .next( this.closeDialog, this ); | ||
| + | } | ||
| + | else if ( action === 'rename' ) { | ||
| + | process.next( this.prepare, this ) | ||
| + | .next( this.checkCategory, this ) | ||
| + | .next( this.getMembers, this ) | ||
| + | .next( this.lockMultitabs, this ) | ||
| + | .next( this.checkLimits, this ) | ||
| + | .next( this.editMembers, this ) | ||
| + | .next( this.renameCategory, this ); | ||
| + | } | ||
| + | else if ( action === 'rbot' ) { | ||
| + | process.next( this.prepare, this ) | ||
| + | .next( this.checkCategory, this ) | ||
| + | .next( this.getMembers, this ) | ||
| + | .next( this.postRBot, this ) | ||
| + | .next( this.renameCategory, this ); | ||
| + | } | ||
| + | |||
| + | if ( options.indexOf( 'post-dr' ) > -1 ) { | ||
| + | process.next( this.postDR, this ); | ||
| + | } | ||
| + | process.next( this.unlockMultitabs, this ) | ||
| + | .next( this.success, this ) | ||
| + | .next( this.closeDialog, this ); | ||
| + | |||
| + | return process; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Close the window. | ||
| + | * | ||
| + | * @return {jQuery.Promise} Promise resolved when window is closed | ||
| + | */ | ||
| + | CatRename.prototype.closeDialog = function () { | ||
| + | var dialog = this; | ||
| + | |||
| + | var lifecycle = dialog.close(); | ||
| + | |||
| + | return lifecycle.closed; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Get the height of the window body. | ||
| + | * Used by the ProcessDialog to set an accurate height to the dialog. | ||
| + | * | ||
| + | * @return {number} Height in px the dialog should be. | ||
| + | */ | ||
| + | CatRename.prototype.getBodyHeight = function () { | ||
| + | return this.configContent.$element.outerHeight( true ); | ||
| + | }; | ||
| + | |||
| + | |||
| + | |||
| + | /* Process step methods */ | ||
| + | |||
| + | /** | ||
| + | * Fetch and validate user's input to make it easily accessible later. | ||
| + | * | ||
| + | * @return {undefined|OO.ui.Error} Error message for the ProcessDialog | ||
| + | * to display, if any. | ||
| + | */ | ||
| + | CatRename.prototype.prepare = function () { | ||
| + | var dialog = this; | ||
| + | |||
| + | this.oldTitle = mw.config.get( 'wgTitle' ); | ||
| + | this.newTitle = this.newNameInput.getValue().trim().replace(/^([Cc]atégorie|[Cc]ategory):/, ''); | ||
| + | this.reason = this.reasonInput.getValue().trim(); | ||
| + | |||
| + | if ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) === -1 ) { | ||
| + | this.newTitle = this.newTitle.charAt( 0 ).toUpperCase() + this.newTitle.slice( 1 ); | ||
| + | } | ||
| + | if ( this.newTitle === this.oldTitle ) { | ||
| + | return new OO.ui.Error( mw.msg( 'catrename-error-same' ) ); | ||
| + | } | ||
| + | if ( mw.Title.makeTitle( 14, this.newTitle ) === null ) { | ||
| + | return new OO.ui.Error( mw.msg( 'catrename-error-invalidtitle' ) ); | ||
| + | } | ||
| + | if ( this.reason === '' ) { | ||
| + | return new OO.ui.Error( mw.msg( 'catrename-error-noreason' ) ); | ||
| + | } | ||
| + | |||
| + | this.oldPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.oldTitle; | ||
| + | this.newPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.newTitle; | ||
| + | |||
| + | // Disable actions button when a process is runing | ||
| + | this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( true ); | ||
| + | this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( true ); | ||
| + | // Except for the cancel button, which behaviour change to cancel the ongoing process | ||
| + | this.getActions().get( { actions: 'cancel' } )[ 0 ].on( 'click', function () { | ||
| + | dialog.errorHandler( mw.msg( 'catrename-error-canceled' ) ); | ||
| + | } ); | ||
| + | |||
| + | return; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Check if it is technically possible to move the category. | ||
| + | * | ||
| + | * Two main checks are performed: | ||
| + | * * Has the user the right to move the category according to the | ||
| + | * protection level? | ||
| + | * * Is the target title free? | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process if | ||
| + | * successful or stopping the process if rejected. | ||
| + | */ | ||
| + | CatRename.prototype.checkCategory = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-checkcategory' ) ); | ||
| + | |||
| + | var restrictionMove = mw.config.get( 'wgRestrictionMove' ); | ||
| + | for ( var i = 0; i < restrictionMove.length; i++ ) { | ||
| + | if ( ! this.userInGroup( restrictionMove[ i ] ) ) { | ||
| + | this.errorHandler( mw.msg( 'catrename-error-protected' ) ); | ||
| + | return this.deferred; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | this.api.get( { | ||
| + | 'action': 'query', | ||
| + | 'format': 'json', | ||
| + | 'formatversion': 2, | ||
| + | 'prop': 'categoryinfo', | ||
| + | 'titles': this.newPageName | ||
| + | } ).then( function ( data ) { | ||
| + | if ( data.query.pages[ 0 ].missing !== true ) { | ||
| + | //TODO: Allow user to move pages without renaming the cat | ||
| + | dialog.errorHandler( mw.msg( 'catrename-error-categoryexist' ) ); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | dialog.deferred.resolve(); | ||
| + | } ).fail( function ( error ) { | ||
| + | dialog.errorHandler( error ); | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Get all pages, files and sub-categories in the source category. | ||
| + | * | ||
| + | * This method populates the attribute 'members'. | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process if | ||
| + | * successful or stopping the process if rejected. | ||
| + | */ | ||
| + | CatRename.prototype.getMembers = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | this.members = []; | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-getmembers' ) ); | ||
| + | |||
| + | function doGetMembers( paramsContinue ) { | ||
| + | var params = { | ||
| + | 'action': 'query', | ||
| + | 'format': 'json', | ||
| + | 'list': 'categorymembers', | ||
| + | 'formatversion': '2', | ||
| + | 'cmtitle': mw.config.get( 'wgPageName' ), | ||
| + | 'cmprop': 'title', | ||
| + | 'cmlimit': 'max', | ||
| + | }; | ||
| + | if ( paramsContinue ) { | ||
| + | $.extend( params, paramsContinue ); | ||
| + | } | ||
| + | |||
| + | dialog.api.get( params ).then( function ( data ) { | ||
| + | |||
| + | var categoryMembers = data.query.categorymembers; | ||
| + | for ( var i = 0; i < categoryMembers.length; i++ ) { | ||
| + | dialog.members.push( categoryMembers[ i ].title ); | ||
| + | } | ||
| + | |||
| + | if ( data[ 'continue' ] ) { | ||
| + | doGetMembers( data[ 'continue' ] ); | ||
| + | } | ||
| + | else { | ||
| + | dialog.deferred.resolve(); | ||
| + | } | ||
| + | } ).fail( function ( error ) { | ||
| + | dialog.errorHandler( error ); | ||
| + | } ); | ||
| + | |||
| + | } | ||
| + | doGetMembers(); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Lock the process while other instances of CatRename are running. | ||
| + | * | ||
| + | * This method acts a bit like the POSIX sem_wait. | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.lockMultitabs = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | if ( this.userInGroup( 'bot' ) ) { | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | this.lockID = 'catrename-' + this.randomString( 16 ); | ||
| + | this.nextTab = null; | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-waitinglock' ) ); | ||
| + | |||
| + | //TODO: check lock timestamp | ||
| + | if ( mw.storage.get( 'catrename-lock' ) === null ) { | ||
| + | mw.storage.set( 'catrename-lock', this.lockID ); | ||
| + | this.deferred.resolve(); | ||
| + | } | ||
| + | else { | ||
| + | $( window ).on( 'storage.catrename.catrename-waiting', function ( event ) { | ||
| + | if ( event.originalEvent.key === 'catrename-lock' && event.originalEvent.newValue === dialog.lockID ) { | ||
| + | $( window ).off( 'storage.catrename-waiting' ); | ||
| + | dialog.deferred.resolve(); | ||
| + | } | ||
| + | } ); | ||
| + | mw.storage.set( 'catrename-addtab', this.lockID ); | ||
| + | } | ||
| + | |||
| + | |||
| + | $( window ).on( 'storage.catrename', function ( event ) { | ||
| + | // if this tab has no successor and a new one appears, add it as our successor | ||
| + | if ( dialog.nextTab === null && event.originalEvent.key === 'catrename-addtab' && event.originalEvent.newValue !== null ) { | ||
| + | dialog.nextTab = event.originalEvent.newValue; | ||
| + | mw.storage.set( dialog.lockID, dialog.nextTab ); | ||
| + | } | ||
| + | // if our successor decides to leave, remove it and take its successor | ||
| + | else if ( dialog.nextTab !== null && event.originalEvent.key === 'catrename-removetab' && event.originalEvent.newValue === dialog.nextTab ) { | ||
| + | dialog.nextTab = mw.storage.get( dialog.nextTab ); | ||
| + | if ( dialog.nextTab !== null ) { | ||
| + | mw.storage.set( dialog.lockID, dialog.nextTab ); | ||
| + | } | ||
| + | else { | ||
| + | mw.storage.remove( dialog.lockID ); | ||
| + | } | ||
| + | } | ||
| + | } ); | ||
| + | |||
| + | window.addEventListener( 'unload', function (e) { | ||
| + | dialog.unlockMultitabs(); | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Check if the daily limit of edits using this script would be reached | ||
| + | * if the move is performed. | ||
| + | * | ||
| + | * In fact, we are not looking realy on a daily basis, but a 24h rolling | ||
| + | * period. | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.checkLimits = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | var yesterday = new Date(); | ||
| + | yesterday.setDate( yesterday.getDate() - 1 ); | ||
| + | |||
| + | if ( this.userInGroup( 'bot' ) ) { | ||
| + | this.noSpammingDelay = 0; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | this.noSpammingDelay = 5000; | ||
| + | if ( this.members.length > 50 ) { | ||
| + | this.noSpammingDelay = 20000; | ||
| + | } | ||
| + | else if ( this.members.length > 10 ) { | ||
| + | this.noSpammingDelay = 10000; | ||
| + | } | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-checklimits' ) ); | ||
| + | |||
| + | this.api.get( { | ||
| + | 'action': 'query', | ||
| + | 'format': 'json', | ||
| + | 'list': 'usercontribs', | ||
| + | 'formatversion': '2', | ||
| + | 'uclimit': 'max', // only query DAILY_LIMIT results ? | ||
| + | 'ucend': yesterday.toISOString(), | ||
| + | 'ucuser': mw.config.get( 'wgUserName' ), | ||
| + | 'ucprop': 'timestamp', | ||
| + | 'uctag': TAG | ||
| + | } ).then( function ( data ) { | ||
| + | |||
| + | if ( data.query.usercontribs.length + dialog.members.length >= DAILY_LIMIT ) { | ||
| + | dialog.errorHandler( mw.msg( 'catrename-error-limitreached', DAILY_LIMIT ), false ); | ||
| + | } | ||
| + | else { | ||
| + | dialog.deferred.resolve(); | ||
| + | } | ||
| + | } ).fail( function ( error ) { | ||
| + | dialog.errorHandler( error ); | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Try to move all the pages inside the 'members' attribute from the old | ||
| + | * to the new category name by fetching and editing their wikicode. | ||
| + | * | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.editMembers = function () { | ||
| + | var dialog = this, | ||
| + | totalPages = this.members.length, | ||
| + | oldCatRegex = this.buildRegex( this.oldTitle ), | ||
| + | newCatRegex = this.buildRegex( this.newTitle ), | ||
| + | summary = mw.msg( 'catrename-summary', this.oldTitle, this.newTitle, this.reason ), | ||
| + | commonPayload = { | ||
| + | summary: summary, | ||
| + | minor: true, | ||
| + | tags: TAG | ||
| + | }; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | if ( this.userInGroup( 'bot' ) ) { | ||
| + | commonPayload[ 'bot' ] = 1; | ||
| + | } | ||
| + | if ( this.optionCheckboxes.getValue().indexOf( 'watch-members' ) > -1 ) { | ||
| + | commonPayload[ 'watchlist' ] = 'watch'; | ||
| + | } | ||
| + | |||
| + | function doEdit() { | ||
| + | var member = dialog.members.pop(); | ||
| + | if ( dialog.deferred.state() !== 'pending' ) { | ||
| + | return; | ||
| + | } | ||
| + | if ( member === undefined ) { | ||
| + | dialog.deferred.resolve(); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | //TODO: a progress-bar ? | ||
| + | dialog.showStatus( mw.msg( 'catrename-status-editmembers', totalPages - dialog.members.length, totalPages ) ); | ||
| + | |||
| + | dialog.api.edit( member, function ( revision ) { | ||
| + | var content = revision.content, | ||
| + | newCatInPageList = content.match( newCatRegex ); | ||
| + | |||
| + | if ( newCatInPageList !== null ) { | ||
| + | dialog.logFailedPages( member, mw.msg( 'catrename-error-categorypresent' ) ); | ||
| + | } | ||
| + | else { | ||
| + | content = content.replace( | ||
| + | oldCatRegex, | ||
| + | '$1[[' + dialog.newPageName + '$6]]' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | return $.extend( { text: content }, commonPayload ); | ||
| + | } ) | ||
| + | .then( function ( result ) { | ||
| + | if ( result.nochange === true ) { | ||
| + | dialog.logFailedPages( member, mw.msg( 'catrename-error-notfound' ) ); | ||
| + | } | ||
| + | |||
| + | setTimeout( doEdit, dialog.noSpammingDelay ); | ||
| + | } ) | ||
| + | .fail( function ( code, data ) { | ||
| + | if ( code === 'protectedpage' ) { | ||
| + | dialog.logFailedPages( member, mw.msg( 'catrename-error-pageprotected' ) ); | ||
| + | doEdit(); | ||
| + | } | ||
| + | else { | ||
| + | dialog.errorHandler( code ); | ||
| + | } | ||
| + | } ); | ||
| + | } | ||
| + | doEdit(); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Move the category itself. | ||
| + | * | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.renameCategory = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-renamecategory' ) ); | ||
| + | |||
| + | var payload = { | ||
| + | 'action': 'move', | ||
| + | 'format': 'json', | ||
| + | 'from': mw.config.get( 'wgPageName' ), | ||
| + | 'to': this.newPageName, | ||
| + | 'reason': this.reason, | ||
| + | 'tags': TAG, | ||
| + | 'formatversion': '2' | ||
| + | }; | ||
| + | |||
| + | var options = this.optionCheckboxes.getValue(); | ||
| + | if ( options.indexOf( 'movetalk' ) > -1 ) { | ||
| + | payload[ 'movetalk' ] = 1; | ||
| + | } | ||
| + | if ( options.indexOf( 'watch' ) > -1 ) { | ||
| + | payload[ 'watchlist' ] = 'watch'; | ||
| + | } | ||
| + | if ( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ) { | ||
| + | if ( options.indexOf( 'leave-redirect' ) === -1 ) { | ||
| + | payload[ 'noredirect' ] = 1; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | this.api.postWithToken( 'csrf', payload ).then( function ( data ) { | ||
| + | dialog.deferred.resolve(); | ||
| + | } ).fail( function ( error ) { | ||
| + | if ( error === 'articleexists' ) { | ||
| + | dialog.errorHandler( mw.msg( 'catrename-error-articleexists', dialog.newPageName ) ); | ||
| + | } | ||
| + | else { | ||
| + | dialog.errorHandler( error ); | ||
| + | } | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Post a deletion request. | ||
| + | * | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.postDR = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-postdr' ) ); | ||
| + | |||
| + | var content = DR_TEMPLATE | ||
| + | .replace( /\$1/g, this.oldTitle ) | ||
| + | .replace( /\$2/g, this.newTitle ) | ||
| + | .replace( /\$3/g, this.reason ) | ||
| + | .replace( /\$4/g, mw.config.get( 'wgUserName' ) ); | ||
| + | |||
| + | this.api.postWithToken( 'csrf', { | ||
| + | 'action': 'edit', | ||
| + | 'format': 'json', | ||
| + | 'title': this.oldPageName, | ||
| + | 'summary': mw.msg( 'catrename-dr-summary' ), | ||
| + | 'tags': TAG, | ||
| + | 'nocreate': 1, | ||
| + | 'prependtext': content, | ||
| + | 'formatversion': '2' | ||
| + | } ).then( function ( data ) { | ||
| + | dialog.deferred.resolve(); | ||
| + | } ).fail( function ( error ) { | ||
| + | dialog.errorHandler( error ); | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Post a move request for the bots. | ||
| + | * | ||
| + | * @return {JQuery.Deferred} Promise telling to continue the process | ||
| + | * when it is its turn to execute. | ||
| + | */ | ||
| + | CatRename.prototype.postRBot = function () { | ||
| + | var dialog = this; | ||
| + | this.deferred = $.Deferred(); | ||
| + | |||
| + | this.showStatus( mw.msg( 'catrename-status-postrbot' ) ); | ||
| + | |||
| + | var content = RBOT_TEMPLATE | ||
| + | .replace( /\$1/g, this.oldTitle ) | ||
| + | .replace( /\$2/g, this.newTitle ) | ||
| + | .replace( /\$3/g, this.reason ) | ||
| + | .replace( /\$4/g, mw.config.get( 'wgUserName' ) ); | ||
| + | |||
| + | this.api.postWithToken( 'csrf', { | ||
| + | 'action': 'edit', | ||
| + | 'format': 'json', | ||
| + | 'title': RBOT_PAGE, | ||
| + | 'summary': mw.msg( 'catrename-rbot-summary' ), | ||
| + | 'tags': TAG, | ||
| + | 'nocreate': 1, | ||
| + | 'appendtext': content, | ||
| + | 'formatversion': '2' | ||
| + | } ).then( function ( data ) { | ||
| + | dialog.deferred.resolve(); | ||
| + | } ).fail( function ( error ) { | ||
| + | dialog.errorHandler( error ); | ||
| + | } ); | ||
| + | |||
| + | return this.deferred; | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Release the lock to allow other instances of CatRename to execute. | ||
| + | * | ||
| + | * This method acts a bit like the POSIX sem_post. | ||
| + | */ | ||
| + | CatRename.prototype.unlockMultitabs = function () { | ||
| + | if ( this.lockID !== undefined ) { | ||
| + | $( window ).off( 'storage.catrename' ); | ||
| + | |||
| + | mw.storage.set( 'catrename-removetab', this.lockID ); //Inform other tabs that we're closing | ||
| + | mw.storage.remove( this.lockID ); //Clean up our mess from the localStorage | ||
| + | |||
| + | // wake up the next tab, or reset if there is none | ||
| + | if ( mw.storage.get( 'catrename-lock' ) === this.lockID ) { | ||
| + | if ( this.nextTab !== null ) { | ||
| + | mw.storage.set( 'catrename-lock', this.nextTab ); | ||
| + | } | ||
| + | else { | ||
| + | mw.storage.remove( 'catrename-lock' ); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | delete this.lockID; | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Method called when all has gone well (yeah !). | ||
| + | */ | ||
| + | CatRename.prototype.success = function () { | ||
| + | var dialog = this; | ||
| + | |||
| + | setTimeout( function () { | ||
| + | window.location = mw.util.getUrl( dialog.newPageName ); | ||
| + | }, 1000 ); | ||
| + | }; | ||
| + | |||
| + | |||
| + | |||
| + | /* Helper Methods */ | ||
| + | |||
| + | /** | ||
| + | * Get information about the current user's groups. | ||
| + | * | ||
| + | * @param {string} groupName Name of the group to check. | ||
| + | * @return {boolean} Whether the current user is in the given group. | ||
| + | */ | ||
| + | CatRename.prototype.userInGroup = function ( groupName ) { | ||
| + | return ( mw.config.get( 'wgUserGroups' ).indexOf( groupName ) > -1 ); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Display a status message inside the main content of the dialog. | ||
| + | * | ||
| + | * @return {string} Status message to display. | ||
| + | */ | ||
| + | CatRename.prototype.showStatus = function ( status ) { | ||
| + | this.statusIndicator.text( status ); | ||
| + | this.$body.children().detach(); | ||
| + | this.$body.append( this.statusContent.$element ); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Raise an error using OO.ui.Error, and reset all what should be. | ||
| + | * | ||
| + | * @param {string} error Error message to display to the user. | ||
| + | * @param {boolean} recoverable Is the error recoverable (default to true). | ||
| + | * @param {boolean} warning Should we raise a warning instead an error (default to false). | ||
| + | */ | ||
| + | CatRename.prototype.errorHandler = function ( error, recoverable, warning ) { | ||
| + | var errorMessage = new OO.ui.Error( error, { recoverable: recoverable || true, warning: warning || false } ); | ||
| + | |||
| + | this.unlockMultitabs(); | ||
| + | this.$body.children().detach(); | ||
| + | this.$body.append( this.configContent.$element ); | ||
| + | |||
| + | this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( false ); | ||
| + | this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( false ); | ||
| + | |||
| + | this.deferred.reject( errorMessage ); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Add a page to the error log. | ||
| + | * | ||
| + | * @param {string} pageName Name (including namespace) of the page. | ||
| + | * @param {string} reason Explaination of the error. | ||
| + | */ | ||
| + | CatRename.prototype.logFailedPages = function ( pageName, reason ) { | ||
| + | var li = $( '<li>' ).text( ' - ' + reason ), | ||
| + | a = $( '<a>' ).attr( 'href', mw.util.getUrl( pageName ) ).text( pageName ); | ||
| + | this.pagesInError.append( li.prepend( a ) ); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Build a regex to extract the link to a given category from wikicode. | ||
| + | * | ||
| + | * @param {string} category Name (without namespace) of the category. | ||
| + | * @return {RegExp} Regex object to extract the given category. | ||
| + | */ | ||
| + | CatRename.prototype.buildRegex = function ( category ) { | ||
| + | var formattedNamespace = mw.config.get( 'wgFormattedNamespaces' )[ 14 ], | ||
| + | isFirstLetterCaseSensitive = ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) > -1 ), | ||
| + | namespace = '(?:[' + formattedNamespace.charAt( 0 ) + formattedNamespace.charAt( 0 ).toLowerCase() + ']' + formattedNamespace.slice( 1 ) + '|[Cc]ategory)'; | ||
| + | |||
| + | category = category.replace( /([\\\^\$\*\+\?\.\|\{\}\[\]\(\)])/g, '\\$1' ); | ||
| + | |||
| + | if ( ! isFirstLetterCaseSensitive ) { | ||
| + | var firstLetter = category.charAt(0); | ||
| + | if ( firstLetter.toUpperCase() !== firstLetter.toLowerCase() ) { | ||
| + | category = '[' + firstLetter.toUpperCase() + firstLetter.toLowerCase() + ']' | ||
| + | + category.slice(1); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | return new RegExp('(\\s*)\\[\\[( |_)*' + namespace + '( |_)*:( |_)*' + category + '( |_)*(\\|[^\\]]*)?\\]\\]', 'g'); | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Generate a random string. | ||
| + | * | ||
| + | * @param {number} length Length of the string to generate. | ||
| + | * @return {string} The generated string. | ||
| + | */ | ||
| + | CatRename.prototype.randomString = function ( length ) { | ||
| + | var result = ''; | ||
| + | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
| + | for ( var i = 0; i < length; ++i ) { | ||
| + | result += chars.charAt( Math.floor( Math.random() * chars.length ) ); | ||
| + | } | ||
| + | return result; | ||
| + | }; | ||
| + | |||
| + | instanceWindowManager = new OO.ui.WindowManager(); | ||
| + | $( 'body' ).append( instanceWindowManager.$element ); | ||
| + | |||
| + | instanceCatRename = new CatRename(); | ||
| + | instanceWindowManager.addWindows( [ instanceCatRename ] ); | ||
| + | } | ||
| + | |||
| + | } ); | ||
| + | } | ||
| + | |||
| + | /* </nowiki> */ | ||
Version du 28 mars 2020 à 20:39
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*!
* Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.3/customize/?id=e0aa72365adbf98a3e77dd207cbec99d)
* Config saved to config.json and https://gist.github.com/e0aa72365adbf98a3e77dd207cbec99d
*/
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(t){"use strict";function e(e){var o=e.attr("data-target");o||(o=e.attr("href"),o=o&&/#[A-Za-z]/.test(o)&&o.replace(/.*(?=#[^\s]*$)/,""));var i=o&&t(o);return i&&i.length?i:e.parent()}function o(o){o&&3===o.which||(t(n).remove(),t(s).each(function(){var i=t(this),n=e(i),s={relatedTarget:this};n.hasClass("open")&&(o&&"click"==o.type&&/input|textarea/i.test(o.target.tagName)&&t.contains(n[0],o.target)||(n.trigger(o=t.Event("hide.bs.dropdown",s)),o.isDefaultPrevented()||(i.attr("aria-expanded","false"),n.removeClass("open").trigger(t.Event("hidden.bs.dropdown",s)))))}))}function i(e){return this.each(function(){var o=t(this),i=o.data("bs.dropdown");i||o.data("bs.dropdown",i=new r(this)),"string"==typeof e&&i[e].call(o)})}var n=".dropdown-backdrop",s='[data-toggle="dropdown"]',r=function(e){t(e).on("click.bs.dropdown",this.toggle)};r.VERSION="3.3.7",r.prototype.toggle=function(i){var n=t(this);if(!n.is(".disabled, :disabled")){var s=e(n),r=s.hasClass("open");if(o(),!r){"ontouchstart"in document.documentElement&&!s.closest(".navbar-nav").length&&t(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(t(this)).on("click",o);var a={relatedTarget:this};if(s.trigger(i=t.Event("show.bs.dropdown",a)),i.isDefaultPrevented())return;n.trigger("focus").attr("aria-expanded","true"),s.toggleClass("open").trigger(t.Event("shown.bs.dropdown",a))}return!1}},r.prototype.keydown=function(o){if(/(38|40|27|32)/.test(o.which)&&!/input|textarea/i.test(o.target.tagName)){var i=t(this);if(o.preventDefault(),o.stopPropagation(),!i.is(".disabled, :disabled")){var n=e(i),r=n.hasClass("open");if(!r&&27!=o.which||r&&27==o.which)return 27==o.which&&n.find(s).trigger("focus"),i.trigger("click");var a=" li:not(.disabled):visible a",l=n.find(".dropdown-menu"+a);if(l.length){var h=l.index(o.target);38==o.which&&h>0&&h--,40==o.which&&h<l.length-1&&h++,~h||(h=0),l.eq(h).trigger("focus")}}}};var a=t.fn.dropdown;t.fn.dropdown=i,t.fn.dropdown.Constructor=r,t.fn.dropdown.noConflict=function(){return t.fn.dropdown=a,this},t(document).on("click.bs.dropdown.data-api",o).on("click.bs.dropdown.data-api",".dropdown form",function(t){t.stopPropagation()}).on("click.bs.dropdown.data-api",s,r.prototype.toggle).on("keydown.bs.dropdown.data-api",s,r.prototype.keydown).on("keydown.bs.dropdown.data-api",".dropdown-menu",r.prototype.keydown)}(jQuery),+function(t){"use strict";function e(e,i){return this.each(function(){var n=t(this),s=n.data("bs.modal"),r=t.extend({},o.DEFAULTS,n.data(),"object"==typeof e&&e);s||n.data("bs.modal",s=new o(this,r)),"string"==typeof e?s[e](i):r.show&&s.show(i)})}var o=function(e,o){this.options=o,this.$body=t(document.body),this.$element=t(e),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,t.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};o.VERSION="3.3.7",o.TRANSITION_DURATION=300,o.BACKDROP_TRANSITION_DURATION=150,o.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},o.prototype.toggle=function(t){return this.isShown?this.hide():this.show(t)},o.prototype.show=function(e){var i=this,n=t.Event("show.bs.modal",{relatedTarget:e});this.$element.trigger(n),this.isShown||n.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',t.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){i.$element.one("mouseup.dismiss.bs.modal",function(e){t(e.target).is(i.$element)&&(i.ignoreBackdropClick=!0)})}),this.backdrop(function(){var n=t.support.transition&&i.$element.hasClass("fade");i.$element.parent().length||i.$element.appendTo(i.$body),i.$element.show().scrollTop(0),i.adjustDialog(),n&&i.$element[0].offsetWidth,i.$element.addClass("in"),i.enforceFocus();var s=t.Event("shown.bs.modal",{relatedTarget:e});n?i.$dialog.one("bsTransitionEnd",function(){i.$element.trigger("focus").trigger(s)}).emulateTransitionEnd(o.TRANSITION_DURATION):i.$element.trigger("focus").trigger(s)}))},o.prototype.hide=function(e){e&&e.preventDefault(),e=t.Event("hide.bs.modal"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),t(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),t.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",t.proxy(this.hideModal,this)).emulateTransitionEnd(o.TRANSITION_DURATION):this.hideModal())},o.prototype.enforceFocus=function(){t(document).off("focusin.bs.modal").on("focusin.bs.modal",t.proxy(function(t){document===t.target||this.$element[0]===t.target||this.$element.has(t.target).length||this.$element.trigger("focus")},this))},o.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",t.proxy(function(t){27==t.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},o.prototype.resize=function(){this.isShown?t(window).on("resize.bs.modal",t.proxy(this.handleUpdate,this)):t(window).off("resize.bs.modal")},o.prototype.hideModal=function(){var t=this;this.$element.hide(),this.backdrop(function(){t.$body.removeClass("modal-open"),t.resetAdjustments(),t.resetScrollbar(),t.$element.trigger("hidden.bs.modal")})},o.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},o.prototype.backdrop=function(e){var i=this,n=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var s=t.support.transition&&n;if(this.$backdrop=t(document.createElement("div")).addClass("modal-backdrop "+n).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",t.proxy(function(t){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(t.target===t.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),s&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!e)return;s?this.$backdrop.one("bsTransitionEnd",e).emulateTransitionEnd(o.BACKDROP_TRANSITION_DURATION):e()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var r=function(){i.removeBackdrop(),e&&e()};t.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",r).emulateTransitionEnd(o.BACKDROP_TRANSITION_DURATION):r()}else e&&e()},o.prototype.handleUpdate=function(){this.adjustDialog()},o.prototype.adjustDialog=function(){var t=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},o.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},o.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth<t,this.scrollbarWidth=this.measureScrollbar()},o.prototype.setScrollbar=function(){var t=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",t+this.scrollbarWidth)},o.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},o.prototype.measureScrollbar=function(){var t=document.createElement("div");t.className="modal-scrollbar-measure",this.$body.append(t);var e=t.offsetWidth-t.clientWidth;return this.$body[0].removeChild(t),e};var i=t.fn.modal;t.fn.modal=e,t.fn.modal.Constructor=o,t.fn.modal.noConflict=function(){return t.fn.modal=i,this},t(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(o){var i=t(this),n=i.attr("href"),s=t(i.attr("data-target")||n&&n.replace(/.*(?=#[^\s]+$)/,"")),r=s.data("bs.modal")?"toggle":t.extend({remote:!/#/.test(n)&&n},s.data(),i.data());i.is("a")&&o.preventDefault(),s.one("show.bs.modal",function(t){t.isDefaultPrevented()||s.one("hidden.bs.modal",function(){i.is(":visible")&&i.trigger("focus")})}),e.call(s,r,this)})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var i=t(this),n=i.data("bs.tooltip"),s="object"==typeof e&&e;!n&&/destroy|hide/.test(e)||(n||i.data("bs.tooltip",n=new o(this,s)),"string"==typeof e&&n[e]())})}var o=function(t,e){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",t,e)};o.VERSION="3.3.7",o.TRANSITION_DURATION=150,o.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},o.prototype.init=function(e,o,i){if(this.enabled=!0,this.type=e,this.$element=t(o),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&t(t.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var n=this.options.trigger.split(" "),s=n.length;s--;){var r=n[s];if("click"==r)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=r){var a="hover"==r?"mouseenter":"focusin",l="hover"==r?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(l+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},o.prototype.getDefaults=function(){return o.DEFAULTS},o.prototype.getOptions=function(e){return e=t.extend({},this.getDefaults(),this.$element.data(),e),e.delay&&"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),e},o.prototype.getDelegateOptions=function(){var e={},o=this.getDefaults();return this._options&&t.each(this._options,function(t,i){o[t]!=i&&(e[t]=i)}),e},o.prototype.enter=function(e){var o=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o)),e instanceof t.Event&&(o.inState["focusin"==e.type?"focus":"hover"]=!0),o.tip().hasClass("in")||"in"==o.hoverState?void(o.hoverState="in"):(clearTimeout(o.timeout),o.hoverState="in",o.options.delay&&o.options.delay.show?void(o.timeout=setTimeout(function(){"in"==o.hoverState&&o.show()},o.options.delay.show)):o.show())},o.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},o.prototype.leave=function(e){var o=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o)),e instanceof t.Event&&(o.inState["focusout"==e.type?"focus":"hover"]=!1),o.isInStateTrue()?void 0:(clearTimeout(o.timeout),o.hoverState="out",o.options.delay&&o.options.delay.hide?void(o.timeout=setTimeout(function(){"out"==o.hoverState&&o.hide()},o.options.delay.hide)):o.hide())},o.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var i=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!i)return;var n=this,s=this.tip(),r=this.getUID(this.type);this.setContent(),s.attr("id",r),this.$element.attr("aria-describedby",r),this.options.animation&&s.addClass("fade");var a="function"==typeof this.options.placement?this.options.placement.call(this,s[0],this.$element[0]):this.options.placement,l=/\s?auto?\s?/i,h=l.test(a);h&&(a=a.replace(l,"")||"top"),s.detach().css({top:0,left:0,display:"block"}).addClass(a).data("bs."+this.type,this),this.options.container?s.appendTo(this.options.container):s.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var d=this.getPosition(),p=s[0].offsetWidth,c=s[0].offsetHeight;if(h){var f=a,u=this.getPosition(this.$viewport);a="bottom"==a&&d.bottom+c>u.bottom?"top":"top"==a&&d.top-c<u.top?"bottom":"right"==a&&d.right+p>u.width?"left":"left"==a&&d.left-p<u.left?"right":a,s.removeClass(f).addClass(a)}var m=this.getCalculatedOffset(a,d,p,c);this.applyPlacement(m,a);var g=function(){var t=n.hoverState;n.$element.trigger("shown.bs."+n.type),n.hoverState=null,"out"==t&&n.leave(n)};t.support.transition&&this.$tip.hasClass("fade")?s.one("bsTransitionEnd",g).emulateTransitionEnd(o.TRANSITION_DURATION):g()}},o.prototype.applyPlacement=function(e,o){var i=this.tip(),n=i[0].offsetWidth,s=i[0].offsetHeight,r=parseInt(i.css("margin-top"),10),a=parseInt(i.css("margin-left"),10);isNaN(r)&&(r=0),isNaN(a)&&(a=0),e.top+=r,e.left+=a,t.offset.setOffset(i[0],t.extend({using:function(t){i.css({top:Math.round(t.top),left:Math.round(t.left)})}},e),0),i.addClass("in");var l=i[0].offsetWidth,h=i[0].offsetHeight;"top"==o&&h!=s&&(e.top=e.top+s-h);var d=this.getViewportAdjustedDelta(o,e,l,h);d.left?e.left+=d.left:e.top+=d.top;var p=/top|bottom/.test(o),c=p?2*d.left-n+l:2*d.top-s+h,f=p?"offsetWidth":"offsetHeight";i.offset(e),this.replaceArrow(c,i[0][f],p)},o.prototype.replaceArrow=function(t,e,o){this.arrow().css(o?"left":"top",50*(1-t/e)+"%").css(o?"top":"left","")},o.prototype.setContent=function(){var t=this.tip(),e=this.getTitle();t.find(".tooltip-inner")[this.options.html?"html":"text"](e),t.removeClass("fade in top bottom left right")},o.prototype.hide=function(e){function i(){"in"!=n.hoverState&&s.detach(),n.$element&&n.$element.removeAttr("aria-describedby").trigger("hidden.bs."+n.type),e&&e()}var n=this,s=t(this.$tip),r=t.Event("hide.bs."+this.type);return this.$element.trigger(r),r.isDefaultPrevented()?void 0:(s.removeClass("in"),t.support.transition&&s.hasClass("fade")?s.one("bsTransitionEnd",i).emulateTransitionEnd(o.TRANSITION_DURATION):i(),this.hoverState=null,this)},o.prototype.fixTitle=function(){var t=this.$element;(t.attr("title")||"string"!=typeof t.attr("data-original-title"))&&t.attr("data-original-title",t.attr("title")||"").attr("title","")},o.prototype.hasContent=function(){return this.getTitle()},o.prototype.getPosition=function(e){e=e||this.$element;var o=e[0],i="BODY"==o.tagName,n=o.getBoundingClientRect();null==n.width&&(n=t.extend({},n,{width:n.right-n.left,height:n.bottom-n.top}));var s=window.SVGElement&&o instanceof window.SVGElement,r=i?{top:0,left:0}:s?null:e.offset(),a={scroll:i?document.documentElement.scrollTop||document.body.scrollTop:e.scrollTop()},l=i?{width:t(window).width(),height:t(window).height()}:null;return t.extend({},n,a,l,r)},o.prototype.getCalculatedOffset=function(t,e,o,i){return"bottom"==t?{top:e.top+e.height,left:e.left+e.width/2-o/2}:"top"==t?{top:e.top-i,left:e.left+e.width/2-o/2}:"left"==t?{top:e.top+e.height/2-i/2,left:e.left-o}:{top:e.top+e.height/2-i/2,left:e.left+e.width}},o.prototype.getViewportAdjustedDelta=function(t,e,o,i){var n={top:0,left:0};if(!this.$viewport)return n;var s=this.options.viewport&&this.options.viewport.padding||0,r=this.getPosition(this.$viewport);if(/right|left/.test(t)){var a=e.top-s-r.scroll,l=e.top+s-r.scroll+i;a<r.top?n.top=r.top-a:l>r.top+r.height&&(n.top=r.top+r.height-l)}else{var h=e.left-s,d=e.left+s+o;h<r.left?n.left=r.left-h:d>r.right&&(n.left=r.left+r.width-d)}return n},o.prototype.getTitle=function(){var t,e=this.$element,o=this.options;return t=e.attr("data-original-title")||("function"==typeof o.title?o.title.call(e[0]):o.title)},o.prototype.getUID=function(t){do t+=~~(1e6*Math.random());while(document.getElementById(t));return t},o.prototype.tip=function(){if(!this.$tip&&(this.$tip=t(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},o.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},o.prototype.enable=function(){this.enabled=!0},o.prototype.disable=function(){this.enabled=!1},o.prototype.toggleEnabled=function(){this.enabled=!this.enabled},o.prototype.toggle=function(e){var o=this;e&&(o=t(e.currentTarget).data("bs."+this.type),o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o))),e?(o.inState.click=!o.inState.click,o.isInStateTrue()?o.enter(o):o.leave(o)):o.tip().hasClass("in")?o.leave(o):o.enter(o)},o.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})};var i=t.fn.tooltip;t.fn.tooltip=e,t.fn.tooltip.Constructor=o,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=i,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var i=t(this),n=i.data("bs.popover"),s="object"==typeof e&&e;!n&&/destroy|hide/.test(e)||(n||i.data("bs.popover",n=new o(this,s)),"string"==typeof e&&n[e]())})}var o=function(t,e){this.init("popover",t,e)};if(!t.fn.tooltip)throw new Error("Popover requires tooltip.js");o.VERSION="3.3.7",o.DEFAULTS=t.extend({},t.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),o.prototype=t.extend({},t.fn.tooltip.Constructor.prototype),o.prototype.constructor=o,o.prototype.getDefaults=function(){return o.DEFAULTS},o.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),o=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof o?"html":"append":"text"](o),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},o.prototype.hasContent=function(){return this.getTitle()||this.getContent()},o.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},o.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var i=t.fn.popover;t.fn.popover=e,t.fn.popover.Constructor=o,t.fn.popover.noConflict=function(){return t.fn.popover=i,this}}(jQuery);
$('body').ready(function() {
var navDisplay = localStorage.getItem('wikiNavDisplay') || "true";
if (navDisplay === "false") {
$("body").addClass("panel-hidden");
$(this).text("►");
}
$('.togglenav').click(function() {
navDisplay = navDisplay === "true" ? "false" : "true";
if (navDisplay === "false") {
$("body").addClass("panel-hidden");
$(this).text("►");
} else {
$("body").removeClass("panel-hidden");
$(this).text("◄");
}
localStorage.setItem('wikiNavDisplay', navDisplay);
});
});
* CatRename
*
* Ajoute un onglet permettant de renommer une catégorie, en déplaçant les pages
* incluses dans celle-ci. Permet de faire faire l'action à un bot en un clic.
*
* {{Projet:JavaScript/Script|CatRename}}
*/
/* <nowiki> */
/* globals mw, OO, $ */
if ( mw.config.get( 'wgNamespaceNumber' ) === 14 ) {
mw.loader.using( 'mediawiki.util', function () {
'use strict';
// Site-related parameters
const TAG = 'RenommageCategorie';
const DAILY_LIMIT = 250;
const RBOT_PAGE = 'Wikipédia:Bot/Requêtes/Catégories';
const DR_TEMPLATE = '{{Suppression Immédiate|raison=Catégorie récemment renommée en [[:Catégorie:$2]] ($3)|utilisateur=$4}}\n\n';
const RBOT_TEMPLATE = '\n{{Déplacement catégorie|ancienne=$1|nouvelle=$2|raison=$3|demandeur=$4}}';
// Literal non-breaking space, for situations where HTML entities can't be used
const NBSP = String.fromCharCode( 0xA0 );
// Messages
const messages = {
'fr': {
'catrename-title': 'Renommer une catégorie',
'catrename-portlet-title': 'CatRename',
'catrename-action-rename': 'Renommer',
'catrename-action-cancel': 'Annuler',
'catrename-action-rbot': '… ou faire faire la tâche par un bot',
'catrename-checkbox-movetalk': 'Renommer aussi la page de discussion associée',
'catrename-checkbox-leave-redirect': 'Laisser une redirection vers le nouveau titre',
'catrename-checkbox-post-dr': 'Déposer une demande de suppression de l\'ancienne catégorie',
'catrename-checkbox-watch': 'Suivre les catégories originale et nouvelle',
'catrename-checkbox-watch-members': 'Suivre les pages modifiées',
'catrename-field-title': 'Nouveau titre' + NBSP + ':',
'catrename-field-reason': 'Motif' + NBSP + ':',
'catrename-summary': 'Remplacement de la catégorie [[Catégorie:$1]] par [[Catégorie:$2]] : $3',
'catrename-dr-summary': 'Demande de suppression après renommage',
'catrename-rbot-summary': 'RBOT : Demande de renommage de catégorie',
'catrename-status-checkcategory': 'Vérification de la catégorie cible',
'catrename-status-getmembers': 'Récupération des pages membres de la catégorie',
'catrename-status-waitinglock': 'En attente de la fin de renommage dans d\'autres onglets',
'catrename-status-checklimits': 'Vérification de la limite journalière',
'catrename-status-editmembers': 'Modification de la page $1 sur $2',
'catrename-status-renamecategory': 'Renommage de la catégorie',
'catrename-status-postdr': 'Dépôt de la demande de suppression',
'catrename-status-postrbot': 'Dépôt de la requête aux bots',
'catrename-error-canceled': 'Le processus de renommage a été annulé.',
'catrename-error-same': 'Le nouveau titre est identique au titre actuel.',
'catrename-error-invalidtitle': 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.',
'catrename-error-noreason': 'Veuillez indiquer un motif pour ce renommage.',
'catrename-error-protected': 'Cette catégorie est protégée, vous n\'êtes pas autorisé à la renommer.',
'catrename-error-categoryexist': 'Il existe déjà une catégorie avec ce nom…',
'catrename-error-limitreached': 'Le renommage de cette catégorie vous ferait faire plus de $1 modifications avec ce script en moins de 24h. Vous pouvez cependant faire une requête aux bots via le bouton en bas à gauche.',
'catrename-error-categorypresent': 'La page contient déjà la nouvelle catégorie.',
'catrename-error-notfound': 'La catégorie n\'a pas été trouvée dans le code de la page, peut-être est-elle incluse via un modèle' + NBSP + '?',
'catrename-error-pageprotected': 'La page est protégée en écriture.',
'catrename-error-articleexists': 'Impossible de déplacer la catégorie, la page de destination «' + NBSP + '$1' + NBSP + '» existe déjà.',
}
};
mw.messages.set( messages.fr );
var lang = mw.config.get( 'wgUserLanguage' );
if ( lang !== 'fr' && lang in messages ) {
mw.messages.set( messages[ lang ] );
}
var isBootstrapped = false;
var instanceWindowManager;
var instanceCatRename;
$( function ( $ ) {
var portlet = mw.util.addPortletLink( 'p-cactions', '#', mw.msg( 'catrename-portlet-title' ) );
$( portlet ).on( 'click', function ( e ) {
e.preventDefault();
mw.loader.using( [ 'oojs-ui', 'mediawiki.storage', 'mediawiki.api' ], function () {
bootstrapOnce();
instanceCatRename.open();
} );
} );
} );
/* Instanciate CatRename and add it to MediaWiki's UI. */
function bootstrapOnce() {
if (isBootstrapped) {
return;
}
isBootstrapped = true;
/**
* Main class of the gadget CatRename, which is displayed as a ProcessDialog
*
* @class
* @extends OO.ui.ProcessDialog
*
* @constructor
*/
var CatRename = function () {
// Initialize config
var config = { size: 'medium' };
// Parent constructor
CatRename.parent.call( this, config );
// Properties
this.api = new mw.Api( { timeout: 7000 } );
this.oldTitle;
this.newTitle;
this.oldPageName;
this.newPageName;
this.reason;
this.deferred;
this.members;
this.lockID;
this.nextTab;
this.noSpammingDelay;
// Graphical properties
this.configContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
this.statusContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
this.newNameInput;
this.reasonInput;
this.optionCheckboxes;
this.layout;
this.$body;
this.statusIndicator;
this.pagesInError;
};
/* Setup */
OO.inheritClass( CatRename, OO.ui.ProcessDialog );
/* Static Properties */
CatRename.static.name = 'catrename';
CatRename.static.title = mw.msg( 'catrename-title' );
CatRename.static.actions = [
{ action: 'rename', label: mw.msg( 'catrename-action-rename' ), flags: [ 'primary', 'progressive' ] },
{ action: 'cancel', label: mw.msg( 'catrename-action-cancel' ), flags: [ 'safe', 'back' ] },
{ action: 'rbot', label: mw.msg( 'catrename-action-rbot' ), flags: 'other' }
];
/* ProcessDialog-related Methods */
/**
* Build the interface displayed inside the ProcessDialog box.
*/
CatRename.prototype.initialize = function () {
CatRename.parent.prototype.initialize.apply( this, arguments );
this.newNameInput = new OO.ui.TextInputWidget( { value: mw.config.get( 'wgTitle' ) } );
this.reasonInput = new OO.ui.TextInputWidget( {
maxLength: 500,
name: 'wpSummary'
} );
this.optionCheckboxes = new OO.ui.CheckboxMultiselectInputWidget( {
value: [ 'movetalk', 'post-dr' ],
options: [
{ data: 'movetalk', label: mw.msg( 'catrename-checkbox-movetalk' ) },
( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ?
{ data: 'leave-redirect', label: mw.msg( 'catrename-checkbox-leave-redirect' ) } :
{ data: 'post-dr', label: mw.msg( 'catrename-checkbox-post-dr' ) }
),
{ data: 'watch', label: mw.msg( 'catrename-checkbox-watch' ) },
{ data: 'watch-members', label: mw.msg( 'catrename-checkbox-watch-members' ) }
]
} );
this.layout = new OO.ui.Widget( {
content: [
new OO.ui.FieldLayout(
this.newNameInput, {
align: 'top',
label: mw.msg( 'catrename-field-title' ),
}
),
new OO.ui.FieldLayout(
this.reasonInput, {
align: 'top',
label: mw.msg( 'catrename-field-reason' ),
}
),
new OO.ui.FieldLayout(
this.optionCheckboxes, {}
)
],
} );
this.configContent.$element.append( this.layout.$element );
this.$body.append( this.configContent.$element );
this.statusIndicator = $( '<h3>' )
.css( 'text-align', 'center' )
.css( 'margin-top', '1em' )
.css( 'margin-bottom', '2em' );
this.pagesInError = $( '<ul>' );
this.statusContent.$element.append( this.statusIndicator ).append( this.pagesInError );
this.setSize( this.size );
this.updateSize();
};
/**
* Get a process for taking action.
*
* This method is called within the ProcessDialog when the user clicks
* on an action button (the one defined in CatRename.static.actions).
* Here is defined in which order each method of the category moving
* process is called.
* @param {string} action Name of the action button clicked.
* @return {OO.ui.Process} Action process.
*/
CatRename.prototype.getActionProcess = function ( action ) {
var process = new OO.ui.Process(),
options = this.optionCheckboxes.getValue();
if ( action === 'cancel' || action === '' ) { // empty string when closing with Escape key
return process.next( this.unlockMultitabs, this )
.next( this.closeDialog, this );
}
else if ( action === 'rename' ) {
process.next( this.prepare, this )
.next( this.checkCategory, this )
.next( this.getMembers, this )
.next( this.lockMultitabs, this )
.next( this.checkLimits, this )
.next( this.editMembers, this )
.next( this.renameCategory, this );
}
else if ( action === 'rbot' ) {
process.next( this.prepare, this )
.next( this.checkCategory, this )
.next( this.getMembers, this )
.next( this.postRBot, this )
.next( this.renameCategory, this );
}
if ( options.indexOf( 'post-dr' ) > -1 ) {
process.next( this.postDR, this );
}
process.next( this.unlockMultitabs, this )
.next( this.success, this )
.next( this.closeDialog, this );
return process;
};
/**
* Close the window.
*
* @return {jQuery.Promise} Promise resolved when window is closed
*/
CatRename.prototype.closeDialog = function () {
var dialog = this;
var lifecycle = dialog.close();
return lifecycle.closed;
};
/**
* Get the height of the window body.
* Used by the ProcessDialog to set an accurate height to the dialog.
*
* @return {number} Height in px the dialog should be.
*/
CatRename.prototype.getBodyHeight = function () {
return this.configContent.$element.outerHeight( true );
};
/* Process step methods */
/**
* Fetch and validate user's input to make it easily accessible later.
*
* @return {undefined|OO.ui.Error} Error message for the ProcessDialog
* to display, if any.
*/
CatRename.prototype.prepare = function () {
var dialog = this;
this.oldTitle = mw.config.get( 'wgTitle' );
this.newTitle = this.newNameInput.getValue().trim().replace(/^([Cc]atégorie|[Cc]ategory):/, '');
this.reason = this.reasonInput.getValue().trim();
if ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) === -1 ) {
this.newTitle = this.newTitle.charAt( 0 ).toUpperCase() + this.newTitle.slice( 1 );
}
if ( this.newTitle === this.oldTitle ) {
return new OO.ui.Error( mw.msg( 'catrename-error-same' ) );
}
if ( mw.Title.makeTitle( 14, this.newTitle ) === null ) {
return new OO.ui.Error( mw.msg( 'catrename-error-invalidtitle' ) );
}
if ( this.reason === '' ) {
return new OO.ui.Error( mw.msg( 'catrename-error-noreason' ) );
}
this.oldPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.oldTitle;
this.newPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.newTitle;
// Disable actions button when a process is runing
this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( true );
this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( true );
// Except for the cancel button, which behaviour change to cancel the ongoing process
this.getActions().get( { actions: 'cancel' } )[ 0 ].on( 'click', function () {
dialog.errorHandler( mw.msg( 'catrename-error-canceled' ) );
} );
return;
};
/**
* Check if it is technically possible to move the category.
*
* Two main checks are performed:
* * Has the user the right to move the category according to the
* protection level?
* * Is the target title free?
* @return {JQuery.Deferred} Promise telling to continue the process if
* successful or stopping the process if rejected.
*/
CatRename.prototype.checkCategory = function () {
var dialog = this;
this.deferred = $.Deferred();
this.showStatus( mw.msg( 'catrename-status-checkcategory' ) );
var restrictionMove = mw.config.get( 'wgRestrictionMove' );
for ( var i = 0; i < restrictionMove.length; i++ ) {
if ( ! this.userInGroup( restrictionMove[ i ] ) ) {
this.errorHandler( mw.msg( 'catrename-error-protected' ) );
return this.deferred;
}
}
this.api.get( {
'action': 'query',
'format': 'json',
'formatversion': 2,
'prop': 'categoryinfo',
'titles': this.newPageName
} ).then( function ( data ) {
if ( data.query.pages[ 0 ].missing !== true ) {
//TODO: Allow user to move pages without renaming the cat
dialog.errorHandler( mw.msg( 'catrename-error-categoryexist' ) );
return;
}
dialog.deferred.resolve();
} ).fail( function ( error ) {
dialog.errorHandler( error );
} );
return this.deferred;
};
/**
* Get all pages, files and sub-categories in the source category.
*
* This method populates the attribute 'members'.
* @return {JQuery.Deferred} Promise telling to continue the process if
* successful or stopping the process if rejected.
*/
CatRename.prototype.getMembers = function () {
var dialog = this;
this.deferred = $.Deferred();
this.members = [];
this.showStatus( mw.msg( 'catrename-status-getmembers' ) );
function doGetMembers( paramsContinue ) {
var params = {
'action': 'query',
'format': 'json',
'list': 'categorymembers',
'formatversion': '2',
'cmtitle': mw.config.get( 'wgPageName' ),
'cmprop': 'title',
'cmlimit': 'max',
};
if ( paramsContinue ) {
$.extend( params, paramsContinue );
}
dialog.api.get( params ).then( function ( data ) {
var categoryMembers = data.query.categorymembers;
for ( var i = 0; i < categoryMembers.length; i++ ) {
dialog.members.push( categoryMembers[ i ].title );
}
if ( data[ 'continue' ] ) {
doGetMembers( data[ 'continue' ] );
}
else {
dialog.deferred.resolve();
}
} ).fail( function ( error ) {
dialog.errorHandler( error );
} );
}
doGetMembers();
return this.deferred;
};
/**
* Lock the process while other instances of CatRename are running.
*
* This method acts a bit like the POSIX sem_wait.
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.lockMultitabs = function () {
var dialog = this;
this.deferred = $.Deferred();
if ( this.userInGroup( 'bot' ) ) {
return;
}
this.lockID = 'catrename-' + this.randomString( 16 );
this.nextTab = null;
this.showStatus( mw.msg( 'catrename-status-waitinglock' ) );
//TODO: check lock timestamp
if ( mw.storage.get( 'catrename-lock' ) === null ) {
mw.storage.set( 'catrename-lock', this.lockID );
this.deferred.resolve();
}
else {
$( window ).on( 'storage.catrename.catrename-waiting', function ( event ) {
if ( event.originalEvent.key === 'catrename-lock' && event.originalEvent.newValue === dialog.lockID ) {
$( window ).off( 'storage.catrename-waiting' );
dialog.deferred.resolve();
}
} );
mw.storage.set( 'catrename-addtab', this.lockID );
}
$( window ).on( 'storage.catrename', function ( event ) {
// if this tab has no successor and a new one appears, add it as our successor
if ( dialog.nextTab === null && event.originalEvent.key === 'catrename-addtab' && event.originalEvent.newValue !== null ) {
dialog.nextTab = event.originalEvent.newValue;
mw.storage.set( dialog.lockID, dialog.nextTab );
}
// if our successor decides to leave, remove it and take its successor
else if ( dialog.nextTab !== null && event.originalEvent.key === 'catrename-removetab' && event.originalEvent.newValue === dialog.nextTab ) {
dialog.nextTab = mw.storage.get( dialog.nextTab );
if ( dialog.nextTab !== null ) {
mw.storage.set( dialog.lockID, dialog.nextTab );
}
else {
mw.storage.remove( dialog.lockID );
}
}
} );
window.addEventListener( 'unload', function (e) {
dialog.unlockMultitabs();
} );
return this.deferred;
};
/**
* Check if the daily limit of edits using this script would be reached
* if the move is performed.
*
* In fact, we are not looking realy on a daily basis, but a 24h rolling
* period.
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.checkLimits = function () {
var dialog = this;
this.deferred = $.Deferred();
var yesterday = new Date();
yesterday.setDate( yesterday.getDate() - 1 );
if ( this.userInGroup( 'bot' ) ) {
this.noSpammingDelay = 0;
return;
}
this.noSpammingDelay = 5000;
if ( this.members.length > 50 ) {
this.noSpammingDelay = 20000;
}
else if ( this.members.length > 10 ) {
this.noSpammingDelay = 10000;
}
this.showStatus( mw.msg( 'catrename-status-checklimits' ) );
this.api.get( {
'action': 'query',
'format': 'json',
'list': 'usercontribs',
'formatversion': '2',
'uclimit': 'max', // only query DAILY_LIMIT results ?
'ucend': yesterday.toISOString(),
'ucuser': mw.config.get( 'wgUserName' ),
'ucprop': 'timestamp',
'uctag': TAG
} ).then( function ( data ) {
if ( data.query.usercontribs.length + dialog.members.length >= DAILY_LIMIT ) {
dialog.errorHandler( mw.msg( 'catrename-error-limitreached', DAILY_LIMIT ), false );
}
else {
dialog.deferred.resolve();
}
} ).fail( function ( error ) {
dialog.errorHandler( error );
} );
return this.deferred;
};
/**
* Try to move all the pages inside the 'members' attribute from the old
* to the new category name by fetching and editing their wikicode.
*
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.editMembers = function () {
var dialog = this,
totalPages = this.members.length,
oldCatRegex = this.buildRegex( this.oldTitle ),
newCatRegex = this.buildRegex( this.newTitle ),
summary = mw.msg( 'catrename-summary', this.oldTitle, this.newTitle, this.reason ),
commonPayload = {
summary: summary,
minor: true,
tags: TAG
};
this.deferred = $.Deferred();
if ( this.userInGroup( 'bot' ) ) {
commonPayload[ 'bot' ] = 1;
}
if ( this.optionCheckboxes.getValue().indexOf( 'watch-members' ) > -1 ) {
commonPayload[ 'watchlist' ] = 'watch';
}
function doEdit() {
var member = dialog.members.pop();
if ( dialog.deferred.state() !== 'pending' ) {
return;
}
if ( member === undefined ) {
dialog.deferred.resolve();
return;
}
//TODO: a progress-bar ?
dialog.showStatus( mw.msg( 'catrename-status-editmembers', totalPages - dialog.members.length, totalPages ) );
dialog.api.edit( member, function ( revision ) {
var content = revision.content,
newCatInPageList = content.match( newCatRegex );
if ( newCatInPageList !== null ) {
dialog.logFailedPages( member, mw.msg( 'catrename-error-categorypresent' ) );
}
else {
content = content.replace(
oldCatRegex,
'$1[[' + dialog.newPageName + '$6]]'
);
}
return $.extend( { text: content }, commonPayload );
} )
.then( function ( result ) {
if ( result.nochange === true ) {
dialog.logFailedPages( member, mw.msg( 'catrename-error-notfound' ) );
}
setTimeout( doEdit, dialog.noSpammingDelay );
} )
.fail( function ( code, data ) {
if ( code === 'protectedpage' ) {
dialog.logFailedPages( member, mw.msg( 'catrename-error-pageprotected' ) );
doEdit();
}
else {
dialog.errorHandler( code );
}
} );
}
doEdit();
return this.deferred;
};
/**
* Move the category itself.
*
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.renameCategory = function () {
var dialog = this;
this.deferred = $.Deferred();
this.showStatus( mw.msg( 'catrename-status-renamecategory' ) );
var payload = {
'action': 'move',
'format': 'json',
'from': mw.config.get( 'wgPageName' ),
'to': this.newPageName,
'reason': this.reason,
'tags': TAG,
'formatversion': '2'
};
var options = this.optionCheckboxes.getValue();
if ( options.indexOf( 'movetalk' ) > -1 ) {
payload[ 'movetalk' ] = 1;
}
if ( options.indexOf( 'watch' ) > -1 ) {
payload[ 'watchlist' ] = 'watch';
}
if ( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ) {
if ( options.indexOf( 'leave-redirect' ) === -1 ) {
payload[ 'noredirect' ] = 1;
}
}
this.api.postWithToken( 'csrf', payload ).then( function ( data ) {
dialog.deferred.resolve();
} ).fail( function ( error ) {
if ( error === 'articleexists' ) {
dialog.errorHandler( mw.msg( 'catrename-error-articleexists', dialog.newPageName ) );
}
else {
dialog.errorHandler( error );
}
} );
return this.deferred;
};
/**
* Post a deletion request.
*
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.postDR = function () {
var dialog = this;
this.deferred = $.Deferred();
this.showStatus( mw.msg( 'catrename-status-postdr' ) );
var content = DR_TEMPLATE
.replace( /\$1/g, this.oldTitle )
.replace( /\$2/g, this.newTitle )
.replace( /\$3/g, this.reason )
.replace( /\$4/g, mw.config.get( 'wgUserName' ) );
this.api.postWithToken( 'csrf', {
'action': 'edit',
'format': 'json',
'title': this.oldPageName,
'summary': mw.msg( 'catrename-dr-summary' ),
'tags': TAG,
'nocreate': 1,
'prependtext': content,
'formatversion': '2'
} ).then( function ( data ) {
dialog.deferred.resolve();
} ).fail( function ( error ) {
dialog.errorHandler( error );
} );
return this.deferred;
};
/**
* Post a move request for the bots.
*
* @return {JQuery.Deferred} Promise telling to continue the process
* when it is its turn to execute.
*/
CatRename.prototype.postRBot = function () {
var dialog = this;
this.deferred = $.Deferred();
this.showStatus( mw.msg( 'catrename-status-postrbot' ) );
var content = RBOT_TEMPLATE
.replace( /\$1/g, this.oldTitle )
.replace( /\$2/g, this.newTitle )
.replace( /\$3/g, this.reason )
.replace( /\$4/g, mw.config.get( 'wgUserName' ) );
this.api.postWithToken( 'csrf', {
'action': 'edit',
'format': 'json',
'title': RBOT_PAGE,
'summary': mw.msg( 'catrename-rbot-summary' ),
'tags': TAG,
'nocreate': 1,
'appendtext': content,
'formatversion': '2'
} ).then( function ( data ) {
dialog.deferred.resolve();
} ).fail( function ( error ) {
dialog.errorHandler( error );
} );
return this.deferred;
};
/**
* Release the lock to allow other instances of CatRename to execute.
*
* This method acts a bit like the POSIX sem_post.
*/
CatRename.prototype.unlockMultitabs = function () {
if ( this.lockID !== undefined ) {
$( window ).off( 'storage.catrename' );
mw.storage.set( 'catrename-removetab', this.lockID ); //Inform other tabs that we're closing
mw.storage.remove( this.lockID ); //Clean up our mess from the localStorage
// wake up the next tab, or reset if there is none
if ( mw.storage.get( 'catrename-lock' ) === this.lockID ) {
if ( this.nextTab !== null ) {
mw.storage.set( 'catrename-lock', this.nextTab );
}
else {
mw.storage.remove( 'catrename-lock' );
}
}
delete this.lockID;
}
};
/**
* Method called when all has gone well (yeah !).
*/
CatRename.prototype.success = function () {
var dialog = this;
setTimeout( function () {
window.location = mw.util.getUrl( dialog.newPageName );
}, 1000 );
};
/* Helper Methods */
/**
* Get information about the current user's groups.
*
* @param {string} groupName Name of the group to check.
* @return {boolean} Whether the current user is in the given group.
*/
CatRename.prototype.userInGroup = function ( groupName ) {
return ( mw.config.get( 'wgUserGroups' ).indexOf( groupName ) > -1 );
};
/**
* Display a status message inside the main content of the dialog.
*
* @return {string} Status message to display.
*/
CatRename.prototype.showStatus = function ( status ) {
this.statusIndicator.text( status );
this.$body.children().detach();
this.$body.append( this.statusContent.$element );
};
/**
* Raise an error using OO.ui.Error, and reset all what should be.
*
* @param {string} error Error message to display to the user.
* @param {boolean} recoverable Is the error recoverable (default to true).
* @param {boolean} warning Should we raise a warning instead an error (default to false).
*/
CatRename.prototype.errorHandler = function ( error, recoverable, warning ) {
var errorMessage = new OO.ui.Error( error, { recoverable: recoverable || true, warning: warning || false } );
this.unlockMultitabs();
this.$body.children().detach();
this.$body.append( this.configContent.$element );
this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( false );
this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( false );
this.deferred.reject( errorMessage );
};
/**
* Add a page to the error log.
*
* @param {string} pageName Name (including namespace) of the page.
* @param {string} reason Explaination of the error.
*/
CatRename.prototype.logFailedPages = function ( pageName, reason ) {
var li = $( '<li>' ).text( ' - ' + reason ),
a = $( '<a>' ).attr( 'href', mw.util.getUrl( pageName ) ).text( pageName );
this.pagesInError.append( li.prepend( a ) );
};
/**
* Build a regex to extract the link to a given category from wikicode.
*
* @param {string} category Name (without namespace) of the category.
* @return {RegExp} Regex object to extract the given category.
*/
CatRename.prototype.buildRegex = function ( category ) {
var formattedNamespace = mw.config.get( 'wgFormattedNamespaces' )[ 14 ],
isFirstLetterCaseSensitive = ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) > -1 ),
namespace = '(?:[' + formattedNamespace.charAt( 0 ) + formattedNamespace.charAt( 0 ).toLowerCase() + ']' + formattedNamespace.slice( 1 ) + '|[Cc]ategory)';
category = category.replace( /([\\\^\$\*\+\?\.\|\{\}\[\]\(\)])/g, '\\$1' );
if ( ! isFirstLetterCaseSensitive ) {
var firstLetter = category.charAt(0);
if ( firstLetter.toUpperCase() !== firstLetter.toLowerCase() ) {
category = '[' + firstLetter.toUpperCase() + firstLetter.toLowerCase() + ']'
+ category.slice(1);
}
}
return new RegExp('(\\s*)\\[\\[( |_)*' + namespace + '( |_)*:( |_)*' + category + '( |_)*(\\|[^\\]]*)?\\]\\]', 'g');
};
/**
* Generate a random string.
*
* @param {number} length Length of the string to generate.
* @return {string} The generated string.
*/
CatRename.prototype.randomString = function ( length ) {
var result = '';
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for ( var i = 0; i < length; ++i ) {
result += chars.charAt( Math.floor( Math.random() * chars.length ) );
}
return result;
};
instanceWindowManager = new OO.ui.WindowManager();
$( 'body' ).append( instanceWindowManager.$element );
instanceCatRename = new CatRename();
instanceWindowManager.addWindows( [ instanceCatRename ] );
}
} );
}
/* </nowiki> */
