MediaWiki:Gadget-QuickResponses.js

Da Semi del Verbo, l'enciclopedia dell'influenza del Vangelo sulla cultura

Nota: dopo aver pubblicato, potrebbe essere necessario pulire la cache del proprio browser per vedere i cambiamenti.

  • Firefox / Safari: tieni premuto il tasto delle maiuscole Shift e fai clic su Ricarica, oppure premi Ctrl-F5 o Ctrl-R (⌘-R su Mac)
  • Google Chrome: premi Ctrl-Shift-R (⌘-Shift-R su un Mac)
  • Internet Explorer / Edge: tieni premuto il tasto Ctrl e fai clic su Aggiorna, oppure premi Ctrl-F5
  • Opera: premi Ctrl-F5.
/**
 * Al caricamento di determinate pagine di servizio, rielabora le segnalazioni
 * con pannelli a tab per agevolare la consultazione di contributi e registri e
 * crea un form per permettere risposte rapide con eventuale commento e dati
 * generici di protezione o blocco elaborati in automatico.
 *
 * @author https://it.wikipedia.org/wiki/Utente:Sakretsu
 */
/* global mediaWiki, jQuery, OO */

( function ( mw, $ ) {
	'use strict';

	// Il nome completo della pagina visitata
	var currentPage = mw.config.get( 'wgPageName' );

	// mw.Api utilizzata per leggere dati e rispondere
	var api = new mw.Api();

	// Limite di performance al numero di segnalazioni più recenti da elaborare
	var limit = 50;

	// Configurazione di variabili per blocchi e protezioni
	var conf = {
		blocks: {
			action: {
				pattern: '<a style="color:#444" href="$1">Blocca</a>',
				pageName: 'Speciale:Blocca/$1'
			},
			getLogsInfo: getBlockInfo,
			links: {
				Discussioni: {
					pageName: 'Discussioni utente:$1',
					selector: '.mw-parser-output'
				},
				Contributi: {
					pageName: 'Speciale:Contributi/$1',
					selector: '.mw-contributions-list, .mw-pager-navigation-bar, .mw-contributions-footer',
					params: { limit: 100 },
					booklet: true
				},
				Cancellati: {
					pageName: 'Speciale:ContributiCancellati/$1',
					selector: '#mw-content-text > ul, .mw-nextlink'
				},
				Registri: {
					pageName: 'Speciale:Registri/$1',
					selector: '#mw-log-deleterevision-submit, .mw-nextlink'
				},
				Blocchi: {
					pageName: 'Speciale:Registri',
					selector: '#mw-log-deleterevision-submit, .mw-nextlink',
					params: { type: 'block', page: 'Utente:$1' }
				}
			},
			negativealert: 'L\'utente non risulta bloccato',
			positivealert: 'L\'utente risulta bloccato',
			regex: /(\{\{ *[Vv]andalo[ \n]*\|)/,
			subj: '.utente-username'
		},
		protections: {
			action: {
				pattern: '<a style="color:#444" href="$1">Imposta protezione</a>',
				pageName: '$1',
				params: { action: 'protect' }
			},
			getLogsInfo: getProtectionInfo,
			edit: {
				pattern: '<a style="color:#444" href="$1">Modifica sezione</a>',
			},
			links: {
				Discussioni: {
					getPageName: function( text ) {
						var mwTitle = new mw.Title.newFromText( text );
						if ( mwTitle && !mwTitle.isTalkPage() ) {
							return mwTitle.getTalkPage().getPrefixedText();
						}
					},
					selector: '.mw-parser-output'
				},
				Cronologia: {
					pageName: '$1',
					selector: '#mw-history-compare, .mw-nextlink',
					params: { action: 'history', limit: 100 },
					booklet: true
				},
				Registri: {
					pageName: 'Speciale:Registri',
					selector: '#mw-log-deleterevision-submit',
					params: { page: '$1' }
				},
			},
			missingexpiry: 'Protezione troppo vecchia, dati mancanti',
			missingunprot: 'Non risultano protezioni rimosse',
			negativealert: 'La pagina non risulta protetta',
			positivealert: 'La pagina risulta protetta',
			regex: /(\{\{ *[Rr]ichiesta protezione[ \n]*\|)/,
			subj: 'b > a'
		}
	};

	// Pagine di servizio su cui caricare lo script con rispettiva configurazione
	var noticeboards = {
		'Wikipedia:Richieste_di_protezione_pagina': conf.protections,
		'Wikipedia:Vandalismi_in_corso': conf.blocks
	};

	/**
	 * Ottiene wikitesto e ID della revisione attualmente visualizzata o,
	 * in alternativa, dell'ultima revisione della pagina di servizio.
	 *
	 * @param {string|null} - ID della revisione visualizzata.
	 * @return {object} - jQuery.Promise.
	 */
	function parseContent( revid ) {
		var params = {
			action: 'parse',
			prop: 'revid|wikitext',
			format: 'json'
		};
		if ( revid ) {
			params.oldid = revid;
		} else {
			params.page = currentPage;
		}
		return api.get( params );
	}

	/**
	 * Recupera i dati di blocco cui un utente è attualmente sottoposto.
	 *
	 * @param {string} user - Il nome dell'utente.
	 * @param {function} infoHandler - La funzione da richiamare con i risultati.
	 */
	function getBlockInfo( user, infoHandler ) {
		api.get( {
			action: 'query',
			list: 'blocks|logevents',
			bkusers: user,
			letype: 'block',
			letitle: 'Utente:' + user,
			lelimit: 1,
			format: 'json'
		} ).done( function ( data ) {
			// correzione del timestamp che via API:Blocks risale al primo blocco
			if ( data.query.logevents[ 0 ] && data.query.logevents[ 0 ].action == 'reblock' ) {
				data.query.blocks[ 0 ].timestamp = data.query.logevents[ 0 ].timestamp;
			}
			infoHandler( data.query.blocks[ 0 ] );
		} ).fail( function ( error ) {
			var msg = 'nome utente malscritto, es. caratteri invisibili';
			OO.ui.alert( 'Errore API: ' + ( error === 'baduser_bkusers' ? msg : error ) );
		} );
	}

	/**
	 * Recupera i dati dell'ultima protezione impostata su una pagina.
	 *
	 * @param {string} page - Il nome della pagina.
	 * @param {function} infoHandler - La funzione da richiamare con i risultati.
	 */
	function getProtectionInfo( page, infoHandler ) {
		api.get( {
			action: 'query',
			list: 'logevents',
			leprop: 'type|user|timestamp|details',
			letitle: page,
			letype: 'protect',
			lelimit: 1,
			format: 'json'
		} ).done( function ( data ) {
			var protection = data.query.logevents[ 0 ];
			if ( protection ) {
				protection.by = protection.user;
				if ( protection.params.details ) {
					var details = protection.params.details[ 0 ];
					if ( new Date() > new Date( details.expiry ) ) {
						protection = undefined;
					} else {
						protection.expiry = details.expiry;
						protection.level = details.level == 'sysop' ?
							'protezione totale' : 'semiprotezione';
					}
				}
			}
			infoHandler( protection );
		} ).fail( function ( error ) {
			OO.ui.alert( 'Errore API: ' + error );
		} );
	}

	/**
	 * Funzione di utilità per formattare le date.
	 *
	 * @param {object} obj - Gli anni/mesi/settimane/giorni in numeri.
	 * @return {array} ret - I valori con rispettiva unità di misura.
	 */
	function formatDateValues( obj ) {
		var ret = [];
		var conv = {
			y: [ 'anno', 'anni' ],
			m: [ 'mese', 'mesi' ],
			w: [ 'settimana', 'settimane' ],
			d: [ 'giorno', 'giorni' ]
		};
		$.each( obj, function( k, v ) {
			if ( v !== 0 ) {
				var unit = v == 1 ? conv[ k ][ 0 ] : conv[ k ][ 1 ];
				ret.push( v + ' ' + unit );
			}
		} );
		return ret;
	}

	/**
	 * Funzione per calcolare la durata di un'azione.
	 * Adattata da https://it.wikipedia.org/wiki/Modulo:Data.
	 *
	 * @param {string} start_date - Il timestamp di inizio.
	 * @param {string} expiration - Il timestamp di scadenza.
	 * @return {string} - La durata calcolata.
	 */
	function getDuration( start_date, expiration ) {
		if ( [ 'infinity', 'infinite' ].indexOf( expiration ) !== -1 ) {
			return 'infinito';
		}
		var monthdays = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
		var d1 = new Date( start_date );
		var d2 = new Date( expiration );
		var d1t = d1.getTimezoneOffset(), d2t = d2.getTimezoneOffset();
		if ( d1t !== d2t ) {
			var d2time = d2.getTime(), diff = 60 * 60 * 1000;
			d2.setTime( d2t > d1t ? d2time - diff : d2time + diff );
		}
		var min = ( d2 - d1 ) / 1000 / 60;
		var h = Math.floor( min / 60 );
		if ( h > 24 && h < 48 ) {
			return h + ' ore';
		} else if ( h < 24 ) {
			if ( min / 60 >= 1 ) {
				min -= h * 60;
				h += h === 1 ? ' ora' : ' ore';
				return h + ( min !== 0 ? ' e ' + Math.round( min ) + ' minuti' : '' );
			} else {
				return Math.round( min ) + ' minuti';
			}
		}
		var d1y = d1.getYear(), d1m = d1.getMonth(), d1d = d1.getDate();
		var d2y = d2.getYear(), d2m = d2.getMonth(), d2d = d2.getDate();
		if ( ( d1y % 100 === 0 ) ? ( d1y % 400 === 0 ) : ( d1y % 4 === 0 ) ) {
			monthdays[1] += 1;
		}
		var obj = {
			y: d2y - d1y,
			m: ( d2m - d1m + 12 ) % 12,
			w: 0,
			d: d2d >= d1d ? d2d - d1d : monthdays[d1m] - d1d + d2d
		};
		if ( obj.y > 0 && ( d1m > d2m || ( d1m == d2m && d1d > d2d ) ) ) {
			obj.y -= 1;
		}
		if ( d1d > d2d ) {
			obj.m = ( obj.m === 0 && d1y < d2y ) ? 11 : obj.m - 1;
		}
		if ( Math.floor( obj.d / 7 ) === obj.d / 7 ) {
			obj.w = obj.d / 7;
			obj.d = 0;
		}
		var arr = formatDateValues( obj );
		if ( arr.length > 1 ) {
			var last = arr.pop();
			return arr + ' e ' + last;
		} else {
			return arr.toString();
		}
	}

	/**
	 * Aggiunge la risposta a una segnalazione nella pagina di servizio.
	 *
	 * @param {number} i - Il numero della segnalazione.
	 * @param {string} text - La risposta da inserire.
	 * @param {object} currentrev - La revisione visualizzata.
	 * @param {function} updateHandler - La funzione che aggiorna lo stato dell'operazione.
	 */
	function editContent( i, msg, currentrev, updateHandler ) {
		var wikitext, newText, revId;
		parseContent().then( function( result ) {
			revId = result.parse.revid;
			wikitext = result.parse.wikitext[ '*' ];
			return api.get( {
				action: 'query',
				titles: currentPage,
				indexpageids: 1,
				prop: 'revisions',
				rvprop: 'ids',
				format: 'json'
			} );
		} ).then( function( result ) {
			i = i * 2 + 2;
			var getSplits = function ( str ) {
				return str.split( conf.regex );
			};
			var sliceSplits = function ( splits, newsplits ) {
				return splits.slice( -( 2 * limit + 1 + ( newsplits || 0 ) ) );
			};
			var splits = getSplits( wikitext );
			var unchanged = true;
			var data = result.query.pages[ result.query.pageids[ 0 ] ].revisions[ 0 ];
			if ( data.revid !== currentrev.revid ) {
				var origsplits = getSplits( currentrev.wikitext[ '*' ] );
				var newsplits = Math.max( 0, splits.length - origsplits.length );
				origsplits = sliceSplits( origsplits );
				splits = sliceSplits( splits, newsplits );
				unchanged = origsplits[ i ].indexOf( splits[ i ].trim() ) !== -1;
			} else {
				splits = sliceSplits( splits );
			}
			if ( data.revid !== revId || !unchanged ) {
				return $.Deferred().reject( 'editconflict' );
			}
			wikitext = wikitext.substring( 0, wikitext.lastIndexOf( splits.join( '' ) ) );
			var match = splits[i].match( /(^[\s\S]+?)(?:(\n:+)(.+))?(\n*$|\n+==[\s\S]+$)/ );
			msg = match[ 2 ] ? match[ 2 ] + ':' + msg : '\n:' + msg;
			splits[i] = match[ 1 ] + ( match[ 2 ] ? match[ 2 ] + match[ 3 ] : '' ) + msg + match[ 4 ];
			newText = wikitext + splits.join( '' );
			return api.postWithToken( 'csrf', {
				action: 'edit',
				title: currentPage,
				text: newText,
				summary: 'segnalazione evasa',
				tags: 'quick-responses',
				watchlist: 'nochange'
			} );
		} ).done( function () {
			updateHandler( true );
		} ).fail( function ( code, data ) {
			updateHandler( false );
			console.log( 'Errore API', code, data );
			OO.ui.alert( code === 'editconflict' ?
				'Errore: conflitto di modifiche' :
				'Errore alla modifica della pagina'
			);
		} );
	}

	/**
	 * Ottiene il confronto tra la revisione indicata e quella precedente.
	 *
	 * @param {object} url - L'url della revisione.
	 * @param {function} diffHandler - La funzione da richiamare col risultato.
	 */
	function getDiff( url, diffHandler ) {
		var oldid = mw.util.getParamValue(
			'oldid',
			url.href
		);
		return api.get( {
			action: 'compare',
			prop: 'diff',
			fromrev: oldid,
			torelative: 'prev'
		} ).then( function( data ) {
			diffHandler( data.compare[ '*' ] );
		} );
	}

	/**
	 * Crea un booklet per sfogliare rapidamente i diff più recenti di
	 * una cronologia o un elenco di contributi.
	 *
	 * @param {object} urls - Gli url da cui estrarre i diff.
	 * @return {object} - Il booklet.
	 */
	function buildBooklet( urls ) {
		var pageIndex = 0;
		var $span = $( '<span></span>' );
		var targetUrl = function( current ) {
			var currentDiff = urls.parents( 'li:eq(' + pageIndex + ')' );
			currentDiff.css( 'background-color', current ? '#f8f2ce' : '' );
			if ( current ) {
				var date = currentDiff.find( 'a.mw-changeslist-date' ).text();
				var user = currentDiff.find( 'a.mw-userlink' ).text();
				var label = 'Cambia diff';
				if ( date ) {
					label += ' <small>(attuale ';
					label += user ? 'di ' + user + ' ' : '';
					label += 'in data ' + date + ')</small>';
				}
				$span.html( label );
			}
		};
		var changePage = function ( direction ) {
			targetUrl( false );
			pageIndex = ( urls.length + pageIndex + direction ) % urls.length;
			bookletLayout.setPage( 'page-' + pageIndex );
			targetUrl( true );
			getDiff( urls[ pageIndex ], function( diff ) {
				$table.html( colgroup + diff );
			} );
		};
		var navigationField = new OO.ui.FieldLayout(
			new OO.ui.ButtonGroupWidget( {
				items: [
					new OO.ui.ButtonWidget( {
						data: 'previous',
						icon: 'previous'
					} ).on( 'click', function () {
						changePage( -1 );
					} ),
					new OO.ui.ButtonWidget( {
						data: 'next',
						icon: 'next'
					} ).on( 'click', function () {
						changePage( 1 );
					} )
				]
			} ),
			{
				label: $span,
				align: 'top'
			}
		);
		var bookletLayout = new OO.ui.BookletLayout( {
			expanded: false,
			menuPosition: 'top'
		} );
		var $table = $( '<table></table>' )
			.addClass('diff diff-contentalign-left');
		var colgroup = '<colgroup><col class="diff-marker">\
			<col class="diff-content"><col class="diff-marker">\
			<col class="diff-content"></colgroup>';
		bookletLayout.addPages( [
			new OO.ui.PageLayout( 'page-1', {
				expanded: false,
				$content: $table
			} )
		] );
		getDiff( urls[ 0 ], function( diff ) {
			$table.html( colgroup + diff );
		} );
		targetUrl( true );
		bookletLayout.$element.prepend( navigationField.$element );
		return bookletLayout.$element;
	}

	/**
	 * Crea tab per consultare agevolmente contributi e registri.
	 *
	 * @param {string} subj - Il nome della pagina o dell'utente da esaminare.
	 * @param {object} subjLinks - I link di utilità del template di segnalazione.
	 * @param {object} $h2 - L'eventuale intestazione della segnalazione.
	 * @return {object} - Il pannello a tab.
	 */
	function buildTabs( subj, subjLinks, $h2 ) {
		var indexLayout = new OO.ui.IndexLayout( {
			expanded: false
		} );
		var panelLayout = new OO.ui.PanelLayout( {
			expanded: false,
			framed: true,
			content: [ indexLayout ]
		} );
		var tabs = [
			new OO.ui.TabPanelLayout( 'default', {
				expanded: false,
				label: subj,
				content: [ subjLinks ]
			} )
		];
		$.each( conf.links, function( k, v ) {
			var tab = new OO.ui.TabPanelLayout( k, {
				expanded: false,
				label: k
			} ).on( 'active', function () {
				var $el = tab.$element;
				if ( tab.isActive() && !$el.text() ) {
					$el.append( '<p><i>Caricamento...</i></p>' );
					var pageName = v.getPageName ? v.getPageName( subj ) :
						v.pageName.replace( /\$1/, subj );
					var params = Object.assign( {}, v.params );
					if ( params && params.page ) {
						params.page = params.page.replace( /\$1/, subj );
					}
					var url = mw.util.getUrl( pageName, params );
					var selector = ' ' + v.selector;
					$( $el ).load( url + selector, function( response, status, xhr ) {
						var notFound = xhr.status === 404 || xhr.status === 400;
						if ( status === 'error' && !notFound ) {
							var error = xhr.status + " " + xhr.statusText;
							$el.html( '<p><i>Errore: ' + error + '</i></p>' );
						} else if ( notFound || !$el.text() ||
								$el.find( '.mw-contributions-footer' ).length &&
								!$el.find( '.mw-contributions-list' ).length ) {
							$el.html( '<p><i>Nessun risultato</i></p>' );
						} else if ( v.booklet ) {
							var urls = $el.find( 'a.mw-changeslist-date' );
							if ( urls.length ) {
								$el.prepend( buildBooklet( urls ) );
							}
						}
						$( '.mw-checkbox-toggle-controls' ).remove();
					} );
				}
			} );
			tabs.push( tab );
		} );
		indexLayout.addTabPanels( tabs );
		if ( conf.edit ) {
			var href = $h2.find( '.mw-editsection a' ).last().attr( 'href' );
			var editOption = new OO.ui.TabOptionWidget( {
				disabled: true,
				label: new OO.ui.HtmlSnippet(
					conf.edit.pattern.replace( /\$1/, href )
				)
			} );
			indexLayout.getTabs().addItems( editOption );
		}
		var actionOption = new OO.ui.TabOptionWidget( {
			disabled: true,
			label: new OO.ui.HtmlSnippet(
				conf.action.pattern.replace( /\$1/,
					mw.util.getUrl(
						conf.action.pageName.replace( /\$1/, subj ),
						conf.action.params
				) )
			)
		} );
		indexLayout.getTabs().addItems( actionOption );
		return panelLayout.$element;
	}

	/**
	 * Crea un pulsante di commutazione per accedere al form di risposta.
	 *
	 * @param {boolean} closedReport - True se la richiesta risulta evasa.
	 * @param {function} toggleHandler - La funzione che anima il form.
	 * @return {object} Il pulsante di commutazione.
	 */
	function buildToggleButton( closedReport, toggleHandler ) {
		var toggleButton = new OO.ui.ToggleButtonWidget( {
			classes: [ 'toggle' ],
			icon: 'previous',
			framed: false,
			label: closedReport ? '' : 'Gestisci segnalazione'
		} );
		toggleButton.on( 'click', function () {
			if ( toggleButton.getValue() === true ) {
				toggleButton.setIcon( 'next' );
			} else {
				toggleButton.setIcon( 'previous' );
			}
			toggleHandler();
		} );
		return toggleButton.$element;
	}

	/**
	 * Crea il form per rispondere rapidamente a una segnalazione.
	 *
	 * @param {boolean} reversedResult - True se è una richiesta di sprotezione.
	 * @param {function} clickHandler - La funzione che implementa i pulsanti.
	 * @return {object} - Il form.
	 */
	function buildForm( reversedResult, clickHandler ) {
		var $wrapper = $( '<div></div>' );
		var fieldset = new OO.ui.FieldsetLayout( {
			classes: [ 'form' ]
		} );
		var input = new OO.ui.MultilineTextInputWidget( {
			autosize: true,
			placeholder: 'Commento facoltativo',
			label: '{{}}'
		} );
		var field = new OO.ui.FieldLayout( input, {
			align: 'top'
		} );
		var button1 = new OO.ui.ButtonWidget( {
			icon: 'check',
			label: 'Fatto'
		} ).on( 'click', function () {
			clickHandler( function ( logsInfo ) {
				if ( !reversedResult ) {
					if ( !logsInfo || logsInfo.action == 'unprotect' ) {
						OO.ui.alert( conf.negativealert );
						return;
					} else if ( !logsInfo.expiry ) {
						OO.ui.alert( conf.missingexpiry );
						return;
					}
				} else if ( logsInfo && logsInfo.action != 'unprotect' ) {
					OO.ui.alert( conf.positivealert );
					return;
				} else if ( !logsInfo ) {
					OO.ui.alert( conf.missingunprot );
					return;
				}
				var comment = input.getValue();
				var currentuser = mw.config.get( 'wgUserName' );
				var msg = '{{fatto}}';
				if ( !reversedResult ) {
					var duration = getDuration( logsInfo.timestamp, logsInfo.expiry );
					msg += ' ', msg += duration == 'infinito' && logsInfo.level ?
						logsInfo.level + ' infinita' :
						duration + ( logsInfo.level ? ' di ' + logsInfo.level : '' );
				}
				msg += currentuser !== logsInfo.by ? ' da ' + logsInfo.by : '';
				msg += comment ? ' - ' + comment : '';
				msg += '--~~' + '~~';
				return { msg: msg, button: button1 };
			} );
		} );
		var button2 = new OO.ui.ButtonWidget( {
			icon: 'close',
			label: 'Non fatto'
		} ).on( 'click', function () {
			clickHandler( function ( logsInfo ) {
				if ( logsInfo && logsInfo.action == 'protect' && !logsInfo.expiry ) {
					OO.ui.alert( conf.missingexpiry );
					return;
				} else if ( !reversedResult && logsInfo && logsInfo.action != 'unprotect' ) {
					OO.ui.alert( conf.positivealert );
					return;
				} else if ( reversedResult && ( !logsInfo || logsInfo.action == 'unprotect' ) ) {
					OO.ui.alert( conf.negativealert );
					return;
				}
				var msg = '{{non fatto}} ' + input.getValue() + '--~~' + '~~';
				return { msg: msg, button: button2 };
			} );
		} );
		var actionfield = new OO.ui.ActionFieldLayout( button1, button2, {
			align: 'inline',
			help: 'Cliccando un pulsante risponderai subito alla segnalazione.\
				Firma ed eventuali dati del provvedimento (autore, durata) saranno inseriti in automatico.\
				Di seguito ti sarà mostrato lo stato dell\'operazione.',
			$overlay: $wrapper
		} );
		fieldset.addItems( [ field, actionfield ] );
		return $wrapper.append( fieldset.$element );
	}

	/**
	 * Avvia il caricamento di form e tab per ogni segnalazione prevista.
	 */
	function loadQuickResponses() {
		var style = '.form-div { display: table; float: right; overflow: hidden }' +
			'.form { padding: 1em } .toggle { display: table-cell } h3 { clear: right }' +
			'.oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout { max-height: 80vh; padding: .5em }' +
			'.oo-ui-panelLayout-framed { clear: right; margin: 1.5em 0 1em }' +
			'.h2-inv { position: absolute; visibility: hidden }';
		mw.util.addCSS( style );
		var currentrev;
		parseContent( mw.config.get( 'wgRevisionId' ) ).done( function ( result ) {
			currentrev = result.parse;
		} );
		mw.hook( 'wikipage.content' ).add( function ( $content ) {
			var outerWidth, width;
			var reports = $content.find( '.report' ).slice( -limit );
			reports.each( function ( i, el ) {
				var subj = $( this ).find( conf.subj ).text().trim();
				var closedReport = $( this ).nextUntil( '.report' ).find( '.image' ).length > 0;
				var reversedResult = $( this ).hasClass( 'unprotect' );
				var $form;
				var $toggleButton = buildToggleButton( closedReport, function () {
					if ( !$form ) {
						$form = buildForm( reversedResult, function ( responseHandler ) {
							conf.getLogsInfo( subj, function ( logsInfo ) {
								var obj = responseHandler( logsInfo );
								if ( obj ) {
									obj.button.setDisabled( true );
									status.html( '<i>Sto effettuando la modifica...</i>' );
									editContent( i, obj.msg, currentrev, function ( success ) {
										if ( success ) {
											status.html( '<i>Modifica effettuata:</i><br />' + obj.msg );
										} else {
											obj.button.setDisabled( false );
											status.html( '<i>Modifica non riuscita.</i>' );
										}
									} );
								}
							} );
						} );
						$form.appendTo( $div );
						if ( !outerWidth ) {
							outerWidth = $form.outerWidth();
							width = $form.width();
						}
						$form.css( { 'margin-right': -outerWidth, 'width': width } );
						var status = $( '<p>' ).appendTo( $form );
					}
					var mr = $form.css( 'marginRight' );
					$form.animate( { marginRight: mr === '0px' ? -outerWidth : 0 } );
				} );
				var $div = $( '<div class="form-div"></div>' ).append( $toggleButton );
				$div.insertAfter( this );
				var $h2 = conf.edit && $( this ).prevAll( 'h2' ).first().addClass( 'h2-inv' );
				var tabs = buildTabs( subj, $( this ).contents(), $h2 );
				$( this ).after( tabs ).hide();
			} );
		} );
	}

	$( function () {
		conf = noticeboards[ currentPage ];
		if ( mw.config.get( 'wgAction' ) === 'view' && conf ) {
			loadQuickResponses();
		}
	} );
}( mediaWiki, jQuery ) );