Benutzer:Schnark/js/mostEdited.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
/**
* Dokumentation unter [[Benutzer:Schnark/js/mostEdited]]
* Original unter [[mw:User:Schnark/mostEdited]]
* <nowiki>
*/
/*global mediaWiki*/
//jscs:disable disallowSpacesInsideParentheses, disallowSpacesInsideArrayBrackets
//jscs:disable disallowSpacesInsideObjectBrackets, disallowSpaceAfterLineComment
( function ( $, mw ) {
//virtual indent
'use strict';
var messages, pagesList, currTime, firstTime;

// since there is no good way to get messages in a user script (yet), jsut put them here

/**
* @var {object} messages for a language
*/

//jscs:disable maximumLineLength
messages = {
	en: {
		// from core
		'minutes': '{{PLURAL:$1|$1 minute|$1 minutes}}',
		'hours': '{{PLURAL:$1|$1 hour|$1 hours}}',
		'days': '{{PLURAL:$1|$1 day|$1 days}}',
		'allpagessubmit': 'Go',
		'namespace': 'Namespace:',
		'invert': 'Invert selection',
		'tooltip-invert': 'Check this box to hide changes within the selected namespace (and the associated namespace if checked)',
		'namespace_association': 'Associated namespace',
		'tooltip-namespace_association': 'Check this box to also include the talk or subject namespace associated with the selected namespace',
		'blanknamespace': '(Main)',
		'namespacesall': 'all',
		'rc-change-size': '$1',
		'rc-change-size-new': '$1 {{PLURAL:$1|byte|bytes}} after change',
		'pagetitle': '$1 - {{SITENAME}}',

		// own messages
		'mostedited-legend': 'Most edited pages options', // legend for options on BlankPage with action=mostedited
		'mostedited': 'Most edited pages', // link in sidebar and title of the page
		'tooltip-n-mostedited': 'Shows the most edited pages', // tooltip for link in sidebar
		'mostedited-submit': 'Show most edited pages', // submit button in RecentChanges
		'mostedited-time': 'Time:', // label for time selection
		'mostedited-edits': '{{PLURAL:$1|$1 edit|$1 edits}} ($2 minor {{PLURAL:$2|edit|edits}})', // $1 - total number of edits to the page/section, $2 - number of minor edits
		'mostedited-users': '{{PLURAL:$1|$1 user|$1 users}} ($2 anonymous {{PLURAL:$2|user|users}})', // $1 - total number of different editors, $2 - number of anonymous editors
		'mostedited-size': 'Size change: $1', // $1 - formatted number
		'mostedited-no-pages': 'There are no pages with $1 or more edits in the selected period.', // $1 - number of edits a page must have at least to get shown
		'mostedited-increasing': 'The number of edits seems to be increasing.',
		'mostedited-unchanging': 'The number of edits seems not to change.',
		'mostedited-decreasing': 'The number of edits seems to be decreasing.',
		'mostedited-changed-period': '(changed to: $1)',
		'mostedited-changed-period-tooltip': 'The period had to be shortened because there are too many edits.',
		'mostedited-show': 'more options', //toggle
		'mostedited-hide': 'less options',
		'mostedited-option-sort': 'Sort by:', //label for sort
		'mostedited-option-sort-edits': 'Number of edits',
		'mostedited-option-sort-users': 'Number of users',
		'mostedited-option-sort-size': 'Size of changes',
		'mostedited-option-max-calls': 'Maximal number of API-calls:', //label for max-calls
		'mostedited-option-limit': 'Maximal number of pages to show:', //label for limit
		'mostedited-option-section-limit': 'Maximal number of sections to show for each page:', //label for section-limit
		'mostedited-option-edits': 'Minimal number of edits to show a page:', //label for edits
		'mostedited-option-section-edits': 'Minimal number of edits to show a section:', //label for section-edits
		'mostedited-reload': 'Reload', //button to reload with current parameters
		'mostedited-tooltip-reload': 'Reload this page to get a bookmarkable URL.' //tooltip
	},

	de: {
		'minutes': '{{PLURAL:$1|$1 Minute|$1 Minuten}}',
		'hours': '{{PLURAL:$1|$1 Stunde|$1 Stunden}}',
		'days': '{{PLURAL:$1|$1 Tag|$1 Tage}}',
		'allpagessubmit': 'Anwenden',
		'namespace': 'Namensraum:',
		'invert': 'Auswahl umkehren',
		'tooltip-invert': 'Dieses Auswahlfeld anklicken, um Änderungen im gewählten Namensraum und, sofern ausgewählt, dem entsprechenden zugehörigen Namensraum auszublenden',
		'namespace_association': 'Zugehöriger Namensraum',
		'tooltip-namespace_association': 'Dieses Auswahlfeld anklicken, um den deiner Auswahl zugehörigen Diskussionsnamensraum, oder im umgekehrten Fall, den zugehörigen Namensraum, mit einzubeziehen',
		'blanknamespace': '(Seiten)',
		'namespacesall': 'alle',
		'rc-change-size': '$1 {{PLURAL:$1|Byte|Bytes}}',
		'rc-change-size-new': '$1 {{PLURAL:$1|Byte|Byte}} nach der Änderung',
		'pagetitle': '$1 – {{SITENAME}}',
		'mostedited-legend': 'Anzeigeoptionen',
		'mostedited': 'Meiste Änderungen',
		'tooltip-n-mostedited': 'Zeigt die Seiten mit den meisten Änderungen an',
		'mostedited-submit': 'Zeige Seiten mit meisten Änderungen',
		'mostedited-time': 'Zeit:',
		'mostedited-edits': '{{PLURAL:$1|$1 Bearbeitung|$1 Bearbeitungen}} ($2 kleinere {{PLURAL:$2|Bearbeitung|Bearbeitungen}})',
		'mostedited-users': '$1 Benutzer ({{PLURAL:$2|$2 anonymer|$2 anonyme}})',
		'mostedited-size': 'Größenänderung: $1',
		'mostedited-no-pages': 'Keine Seite wurde im ausgewählten Zeitraum $1 Mal oder häufiger bearbeitet.',
		'mostedited-increasing': 'Die Anzahl der Bearbeitungen scheint zuzunehmen.',
		'mostedited-unchanging': 'Die Anzahl der Bearbeitungen scheint gleich zu bleiben.',
		'mostedited-decreasing': 'Die Anzahl der Bearbeitungen scheint abzunehmen.',
		'mostedited-changed-period': '(geändert in: $1)',
		'mostedited-changed-period-tooltip': 'Die Zeit musste gekürzt werden, da zu viele Bearbeitungen stattfanden.',
		'mostedited-show': 'mehr Optionen',
		'mostedited-hide': 'weniger Optionen',
		'mostedited-option-sort': 'Sortieren nach:',
		'mostedited-option-sort-edits': 'Anzahl der Bearbeitungen',
		'mostedited-option-sort-users': 'Anzahl der Benutzer',
		'mostedited-option-sort-size': 'Größe der Änderungen',
		'mostedited-option-max-calls': 'Maximale Zahl von API-Aufrufen:',
		'mostedited-option-limit': 'Maximale Zahl angezeigter Seiten:',
		'mostedited-option-section-limit': 'Maximale Zahl angezeigter Abschnitte pro Seite:',
		'mostedited-option-edits': 'Minimale Zahl an Bearbeitungen pro Seite:',
		'mostedited-option-section-edits': 'Minimale Zahl an Bearbeitungen pro Abschnitt:',
		'mostedited-reload': 'Neu laden',
		'mostedited-tooltip-reload': 'Lade die Seite erneut, um ein Lesezeichen auf die Seite setzen zu können.'
	},

	'de-ch': {
		'mostedited-size': 'Grössenänderung: $1',
		'mostedited-option-sort-size': 'Grösse der Änderungen'
	},

	'de-formal': {
		'mostedited-tooltip-reload': 'Laden Sie die Seite erneut, um ein Lesezeichen auf die Seite setzen zu können.'
	}
};
//jscs:enable maximumLineLength

/**
* set messages for the user's language
* @param l10n {object} Object with messages for each language
* @param keep {array} Messages that shouldn't be overridden if present
*     (because they are from core and could be loaded by some other script)
*/
function initL10N ( l10n, keep ) {
	var i, chain = mw.language.getFallbackLanguageChain();
	keep = $.grep( mw.messages.get( keep ), function ( val ) {
		return val !== null;
	} );
	for ( i = chain.length - 1; i >= 0; i-- ) {
		if ( chain[ i ] in l10n ) {
			mw.messages.set( l10n[ chain[ i ] ] );
		}
	}
	mw.messages.set( keep );
}

/**
* @var {object} pagesList contains information for every edited page in the form
*	'Pagename': {
*		oldsize: 1234, // size of oldest version
*		newsize: 4321, // size of newest version
*		edits: 123, // number of edits
*		minor: 23, // number of minor edits
*		users: [ 'A', 'B' ], // all editors
*		anons: 5, // number of anonymous editors
*		time: 987654321, // sum of all timestamps (in seconds before now)
*		sections: { // data for each section
*			'Section A': {
*				edits: 12,
*				minor: 2,
*				users: [ 'A' ],
*				anons: 2,
*				time: 87654321
*			}
*		}
*	}
*/

pagesList = {};

/**
* @var {number} current time, time of first edit (milliseconds since 1970-01-01)
*/
currTime = 0;
firstTime = 0;

// helper and format functions

/**
* pad a number with a leading 0 if neccessary
* @param n {number} number to pad
* @return {string}
*/
function pad ( n ) {
	return n < 10 ? '0' + String( n ) : String( n );
}

/**
* converts a timestamp (YYYY-MM-DDTHH:MM:SSZ) into milliseconds since 1970-01-01
* @param timestamp {string}
* @return {number}
*/
function getTime ( timestamp ) {
	return ( new Date( timestamp.slice( 0, 4 ), timestamp.slice( 5, 7 ) - 1, timestamp.slice( 8, 10 ),
		timestamp.slice( 11, 13 ), timestamp.slice( 14, 16 ), timestamp.slice( 17, 19 ) ) ).getTime();
}

/**
* validates an input to ensure it contains an integer
* @param $input {jQuery} input to read from
* @param def {number} default value to use instead
* @return {number} validated number, this number is also put in the input
*/
function validateNumber ( $input, def ) {
	var val = $input.val() || '';
	val = val.trim();
	if ( /^\d+$/.test( val ) ) {
		val = Number( val );
	} else {
		val = 0;
	}
	if ( val === 0 ) {
		val = def;
	}
	$input.val( String( val ) );
	return val;
}

/**
* formats a size change (see ChangesList.php for the original)
* @param diff {number} difference between old and new size
* @param newsize {number} new size
* @return {string} formatted HTML
*/
function showCharacterDifference ( diff, newsize ) {
	var cssClass, sign = '';
	if ( diff < 0 ) {
		cssClass = 'mw-plusminus-neg';
	} else if ( diff > 0 ) {
		sign = '+';
		cssClass = 'mw-plusminus-pos';
	} else {
		cssClass = 'mw-plusminus-null';
	}
	return mw.html.element( 'span',
		{ 'class': cssClass, dir: 'ltr', title: mw.msg( 'rc-change-size-new', mw.language.convertNumber( newsize ) ) },
		sign + mw.msg( 'rc-change-size', mw.language.convertNumber( diff ) ) );
}

/**
* determins whether the edits are increasing or decreasing
* @param data {object} object with entries edits and time
* @return {string} arrow symbol in a <span> with class and tooltip
*/
function getTrend ( data ) {
	var avgTime = data.time / data.edits,
		ratio = avgTime / ( firstTime - currTime ),
		cssClass, tooltipMsg, arrow;
	if ( ratio < 0.4 ) {
		cssClass = 'mw-plusminus-pos';
		tooltipMsg = 'mostedited-increasing';
		arrow = $( 'body' ).is( '.rtl' ) ? '↖' : '↗';
	} else if ( ratio < 0.6 ) {
		cssClass = 'mw-plusminus-null';
		tooltipMsg = 'mostedited-unchanging';
		arrow = $( 'body' ).is( '.rtl' ) ? '←' : '→';
	} else {
		cssClass = 'mw-plusminus-neg';
		tooltipMsg = 'mostedited-decreasing';
		arrow = $( 'body' ).is( '.rtl' ) ? '↙' : '↘';
	}
	return mw.html.element( 'span', { 'class': cssClass, title: mw.msg( tooltipMsg ) }, arrow );
}

/**
* formats a period of time
* @param hours {number} hours
* @return {string} formatted period
*/
function formatPeriod ( hours ) {
	var dayString = '', hourString = '', minuteString = '',
		days, wholeHours, minutes;
	if ( hours >= 24 ) {
		days = Math.floor( hours / 24 );
		hours -= days * 24;
		hours = Math.round( hours );
		if ( hours === 24 ) {
			days += 1;
			hours = 0;
		}
		dayString = mw.msg( 'days', days );
		if ( hours > 0 ) {
			hourString = ' ' + mw.msg( 'hours', hours );
		}
		return dayString + hourString;
	} else {
		wholeHours = Math.floor( hours );
		minutes = Math.round( 60 * ( hours - wholeHours ) );
		if ( minutes === 60 ) {
			wholeHours += 1;
			minutes = 0;
		}
		if ( wholeHours > 0 ) {
			hourString = mw.msg( 'hours', wholeHours );
		}
		if ( wholeHours === 0 || minutes > 0 ) {
			minuteString = mw.msg( 'minutes', minutes );
		}
		if ( hourString !== '' && minuteString !== '' ) {
			hourString += ' ';
		}
		return hourString + minuteString;
	}
}

// main

/**
* This function gets all recent changes in the namespaces starting at the start time.
* After the last API call has been done it will call the callback function
* @param end {string} end time (timestamp)
* @param namespaces {string} namespaces to show, either empty for all or something like '0|1|5'
* @param maxcalls {number} maximal number of API calls
* @param callback {function} function after the last API call, called with one parameter:
*	true if all edits until end were retrieved, false if aborted earlier
* @param cont {object} continue parameter and value, leave empty for first call
*/
function getAPIRecentChanges ( end, namespaces, maxcalls, callback, cont ) {
	var data = {
		action: 'query',
		list: 'recentchanges',
		rcend: end,
		rclimit: 'max',
		rcprop: 'user|comment|title|sizes|flags|timestamp',
		rctype: 'edit|new',
		format: 'json',
		formatversion: 2
	};
	if ( cont ) {
		$.extend( data, cont );
	} else {
		data[ 'continue' ] = '';
		data.curtimestamp = true;
	}
	if ( namespaces ) {
		data.rcnamespace = namespaces;
	}
	$.getJSON( mw.util.wikiScript( 'api' ), data ).then( function ( json ) {
		var rc, i, edit, time, section;
		if ( json && json.curtimestamp ) {
			currTime = getTime( json.curtimestamp );
		}
		if ( json && json.query && json.query.recentchanges ) {
			rc = json.query.recentchanges;
			for ( i = 0; i < rc.length; i++ ) {
				edit = rc[ i ];
				if ( !( edit.title in pagesList ) ) {
					pagesList[ edit.title ] = {
						newsize: edit.newlen, // the first is the latest edit, so newlen is the most recent size
						edits: 0,
						minor: 0,
						users: [],
						anons: 0,
						time: 0,
						sections: {}
					};
				}
				section = /^\/\*\s*(.*?)\s*\*\//.exec( edit.comment ); // title of the section
				if ( section ) {
					section = section[ 1 ];
				}
				if ( section ) {
					if ( !( section in pagesList[ edit.title ].sections ) ) {
						pagesList[ edit.title ].sections[ section ] = {
							edits: 0,
							minor: 0,
							users: [],
							anons: 0,
							time: 0
						};
					}
				}
				time = getTime( edit.timestamp );
				firstTime = time;
				pagesList[ edit.title ].edits++; // increment edits
				if ( section ) {
					pagesList[ edit.title ].sections[ section ].edits++;
				}
				if ( edit.minor ) { // increment minor edits
					pagesList[ edit.title ].minor++;
					if ( section ) {
						pagesList[ edit.title ].sections[ section ].minor++;
					}
				}
				// update oldlen for every edit, only the earliest (= last) is interesting
				pagesList[ edit.title ].oldsize = edit.oldlen;
				if ( pagesList[ edit.title ].users.indexOf( edit.user ) === -1 ) { // store if new user
					pagesList[ edit.title ].users.push( edit.user );
					if ( edit.anon ) {
						pagesList[ edit.title ].anons++;
					}
				}
				if ( section ) {
					if ( pagesList[ edit.title ].sections[ section ].users.indexOf( edit.user ) === -1 ) {
						pagesList[ edit.title ].sections[ section ].users.push( edit.user );
						if ( edit.anon ) {
							pagesList[ edit.title ].sections[ section ].anons++;
						}
					}
				}
				pagesList[ edit.title ].time += ( time - currTime );
				if ( section ) {
					pagesList[ edit.title ].sections[ section ].time += ( time - currTime );
				}
			}
		}
		if ( json && json[ 'continue' ] ) {
			if ( maxcalls > 1 ) {
				getAPIRecentChanges( end, namespaces, maxcalls - 1, callback, json[ 'continue' ] );
			} else {
				callback( false );
			}
		} else {
			callback( true );
		}
	} );
}

/**
* get the pages/sections with the most edits
* @param data {object} data about the number of edits, entries must have the form
*	'Name': {edits: 123}
* @param count {number} number of pages/sections to get
* @param edits {number} number of edits needed at least to output a page/section
* @param sortBy {string} criterion to sort pages by
* @return {array} list of the pages/sections with the most edits (decreasing order)
*/

function getMostEdited ( data, count, edits, sortBy ) {
	var items = [], item, output, sortFunction;
	for ( item in data ) {
		if ( data.hasOwnProperty( item ) ) {
			items.push( item );
		}
	}
	//jscs:disable requireFunctionDeclarations
	switch ( sortBy ) {
	case 'edits':
		sortFunction = function ( a, b ) {
			return data[ b ].edits - data[ a ].edits;
		};
		break;
	case 'users':
		sortFunction = function ( a, b ) {
			return data[ b ].users.length - data[ a ].users.length;
		};
		break;
	case 'size':
		sortFunction = function ( a, b ) {
			return Math.abs( data[ b ].oldsize - data[ b ].newsize ) - Math.abs( data[ a ].oldsize - data[ a ].newsize );
		};
	}
	//jscs:enable requireFunctionDeclarations
	items.sort( sortFunction );
	output = items.slice( 0, count );
	while ( sortBy === 'edits' && output.length > 0 && data[ output[ output.length - 1 ] ].edits < edits ) {
		output.pop();
	}
	return output;
}

// functions generating HTML

//for output

/**
* generate list with information about edits for a page/section
* @param edits {number} number of edits
* @param minor {number} number of minor edits
* @param users {array} users that edited the page/section
* @param anons {number} number of anonymous users
* @param oldsize {number} old size (or undefined for sections)
* @param newsize {number} new size (or undefined for sections)
* @param href {string} for pages: href of action=history
* @return {string} HTML <ul>
*/
function generateListHTML ( edits, minor, users, anons, oldsize, newsize, href ) {
	var html = '', editsHTML = mw.msg( 'mostedited-edits', edits, minor );
	if ( href ) {
		editsHTML = mw.html.element( 'a', { href: href }, new mw.html.Raw( editsHTML ) );
	}
	html += mw.html.element( 'li', {}, new mw.html.Raw( editsHTML ) );
	html += mw.html.element( 'li', { title: users.join( ', ' ) },
		new mw.html.Raw( mw.msg( 'mostedited-users', users.length, anons ) ) );
	if ( oldsize !== undefined && newsize !== undefined ) {
		html += mw.html.element( 'li', {}, new mw.html.Raw(
			mw.msg( 'mostedited-size', showCharacterDifference( newsize - oldsize, newsize ) ) ) );
	}
	return mw.html.element( 'ul', {}, new mw.html.Raw( html ) );
}

/**
* get HTML for one entry
* @param page {string} name of the page to get HTML for
* @param count {number} number of sections to show
* @param edits {number} number of edits a section must have at least
* @return {string} HTML
*/
function generatePageHTML ( page, count, edits ) {
	var data = pagesList[ page ],
		html, sections, i, section, sectionData, history;

	history = mw.util.getUrl( page, { action: 'history' } );

	html = mw.html.element( 'h2', {}, new mw.html.Raw(
			mw.html.element( 'a', { href: mw.util.getUrl( page ), title: page }, page ) +
			getTrend( data ) ) );
	html += generateListHTML( data.edits, data.minor, data.users, data.anons, data.oldsize, data.newsize, history );

	sections = getMostEdited( data.sections, count, edits, 'edits' );
	for ( i = 0; i < sections.length; i++ ) {
		section = sections[ i ];
		sectionData = data.sections[ section ];
		html += mw.html.element( 'h3', {}, new mw.html.Raw(
			mw.html.element( 'a',
				{ href: mw.util.getUrl( page + '#' + section ) },
				section ) +
			getTrend( sectionData ) ) );
		html += generateListHTML( sectionData.edits, sectionData.minor, sectionData.users, sectionData.anons );
	}
	return html;
}

/**
* get HTML for complete list
* @param countPages {number} number of pages to show
* @param countSections {number} number of sections to show for each page
* @param editPages {number} number of edits a page must have at least
* @param editSections {number} number of edits a section must have at least
* @param sortBy {string} criterion to sort pages by
* @return {string} HTML
*/
function generateHTML ( countPages, countSections, editPages, editSections, sortBy ) {
	var pages = getMostEdited( pagesList, countPages, editPages, sortBy ),
		html = '',
		i;
	if ( pages.length === 0 ) {
		html += mw.msg( 'mostedited-no-pages', editPages );
	} else {
		for ( i = 0; i < pages.length; i++ ) {
			html += generatePageHTML( pages[ i ], countSections, editSections );
		}
	}
	return html;
}

//for input

/**
* get HTML for the time select
* @return {string} HTML
*/
function generateTimeSelect () {
	var i, hours, times, optionsTime = [], labelTime, selectTime;
	labelTime = mw.html.element( 'label', { 'for': 'time' }, mw.msg( 'mostedited-time' ) );
	hours = parseFloat( mw.util.getParamValue( 'hours' ) || '0', 10 );
	if ( hours <= 0 ) {
		hours = 1;
	}
	times = [ 0.25, 0.5, 1, 2, 24 ];
	if ( times.indexOf( hours ) === -1 ) {
		times.push( hours );
		times.sort( function ( a, b ) {
			return a - b;
		} );
	}
	for ( i = 0; i < times.length; i++ ) {
		optionsTime.push( mw.html.element( 'option',
			{ value: times[ i ], selected: times[ i ] === hours },
			formatPeriod( times[ i ] ) ) );
	}
	selectTime = mw.html.element( 'select',
		{ id: 'time', name: 'time', 'class': 'timeselector' },
		new mw.html.Raw( optionsTime.join( '' ) ) );
	return '<tr><td class="mw-label">' +
		labelTime +
		'</td><td class="mw-input">' +
		selectTime +
		' <span id="mostedited-real-time"></span>' +
		'</td></tr>';
}

/**
* get HTML for the namespace select
* @return {string} HTML
*/
function generateNamespaceSelect () {
	var i, formattedNamespaces, namespace, optionsNamespaces = [], labelNamespaces, selectNamespaces, invert, associated, cls;
	formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' );
	labelNamespaces = mw.html.element( 'label', { 'for': 'namespace' }, mw.msg( 'namespace' ) );
	optionsNamespaces.push( mw.html.element( 'option', { value: '' }, mw.msg( 'namespacesall' ) ) );
	for ( i in formattedNamespaces ) {
		if ( formattedNamespaces.hasOwnProperty( i ) && i >= 0 ) {
			namespace = formattedNamespaces[ i ];
			if ( namespace === '' ) {
				namespace = mw.msg( 'blanknamespace' );
			}
			optionsNamespaces.push( mw.html.element( 'option',
				{ value: i, selected: mw.util.getParamValue( 'namespace' ) === i }, // both are strings
				namespace ) );
		}
	}
	selectNamespaces = mw.html.element( 'select',
		{ id: 'namespace', name: 'namespace', 'class': 'namespaceselector' },
		new mw.html.Raw( optionsNamespaces.join( '' ) ) );
	invert = mw.html.element( 'input',
		{ name: 'invert', value: 1, id: 'nsinvert', type: 'checkbox', title: mw.msg( 'tooltip-invert' ),
			checked: mw.util.getParamValue( 'invert' ) === '1' } ) +
		'&nbsp;' +
		mw.html.element( 'label', { 'for': 'nsinvert', title: mw.msg( 'tooltip-invert' ) }, mw.msg( 'invert' ) );
	associated = mw.html.element( 'input',
		{ name: 'associated', value: 1, id: 'nsassociated', type: 'checkbox',
			title: mw.msg( 'tooltip-namespace_association' ),
			checked: mw.util.getParamValue( 'associated' ) === '1' } ) +
		'&nbsp;' +
		mw.html.element( 'label',
			{ 'for': 'nsassociated', title: mw.msg( 'tooltip-namespace_association' ) },
			mw.msg( 'namespace_association' ) );
	cls = [ 'mw-input-with-label' ];
	if ( !mw.util.getParamValue( 'namespace' ) ) {
		cls.push( 'mw-input-hidden' );
	}
	cls = cls.join( ' ' );
	invert = mw.html.element( 'span', { 'class': cls }, new mw.html.Raw( invert ) );
	associated = mw.html.element( 'span', { 'class': cls }, new mw.html.Raw( associated ) );
	return '<tr><td class="mw-label">' +
		labelNamespaces +
		'</td><td class="mw-input">' +
		selectNamespaces +
		' ' +
		invert +
		' ' +
		associated +
		'</td></tr>';
}

/**
* get HTML for the sort select
* @param html {string} HTML to insert after the select
* @return {string} HTML
*/
function generateSortSelect ( html ) {
	var sortParam, labelSort, selectSort;
	labelSort = mw.html.element( 'label', { 'for': 'mostedited-sort' }, mw.msg( 'mostedited-option-sort' ) );
	sortParam = mw.util.getParamValue( 'sort' ) || 'edits';
	selectSort = mw.html.element( 'select', { id: 'mostedited-sort' }, new mw.html.Raw(
		mw.html.element( 'option', { value: 'edits', selected: sortParam === 'edits' },
			mw.msg( 'mostedited-option-sort-edits' ) ) +
		mw.html.element( 'option', { value: 'users', selected: sortParam === 'users' },
			mw.msg( 'mostedited-option-sort-users' ) ) +
		mw.html.element( 'option', { value: 'size', selected: sortParam === 'size' },
			mw.msg( 'mostedited-option-sort-size' ) )
	) );
	return '<tr><td class="mw-label">' +
		labelSort +
		'</td><td class="mw-input">' +
		selectSort +
		html +
		'</td></tr>';
}
/**
* generate HTML for an input (used for advanced options)
* @param id {string} ID of input
* @param labelMsg {string} message for label
* @param URLParam {string} name of URL parameter
* @param def {number} default value
* @return {string} HTML
*/
function generateInput ( id, labelMsg, URLParam, def ) {
	return '<tr><td class="mw-label">' +
		mw.html.element( 'label', { 'for': id }, mw.msg( labelMsg ) ) +
		'</td><td class="mw-input">' +
		mw.html.element( 'input', { type: 'text', id: id, 'class': 'noime', size: 3,
			value: mw.util.getParamValue( URLParam ) || def } ) +
		'</td></tr>';
}

/**
* get HTML for header
* @return {string} HTML
*/
function generateHeaderHTML () {
	$( '#firstHeading' ).text( mw.msg( 'mostedited' ) );
	var html, legend, submit;
	legend = mw.html.element( 'legend', {}, mw.msg( 'mostedited-legend' ) );
	submit = mw.html.element( 'input', { type: 'button', id: 'submitButton', value: mw.msg( 'allpagessubmit' ) } );
	html = '<fieldset class="rcoptions">' + // structure copied from HTML of Special:RecentChanges
		legend +
		'<table class="mw-recentchanges-table"><tbody>' +
		generateTimeSelect() +
		generateNamespaceSelect() +
		generateSortSelect( ' ' + submit ) +
		'</tbody></table>' +
		mw.html.element( 'div', { id: 'advanced-options', 'class': 'mw-collapsed',
			'data-collapsetext': mw.msg( 'mostedited-hide' ), 'data-expandtext': mw.msg( 'mostedited-show' ) },
			new mw.html.Raw(
				'<table><tbody>' +
				generateInput( 'mostedited-input-max-calls', 'mostedited-option-max-calls', 'max-calls', 5 ) +
				generateInput( 'mostedited-input-limit', 'mostedited-option-limit', 'limit', 10 ) +
				generateInput( 'mostedited-input-section-limit', 'mostedited-option-section-limit', 'section-limit', 3 ) +
				generateInput( 'mostedited-input-edits', 'mostedited-option-edits', 'edits', 2 ) +
				generateInput( 'mostedited-input-section-edits', 'mostedited-option-section-edits', 'section-edits', 2 ) +
				'</tbody></table>' +
				mw.html.element( 'input', { type: 'button', id: 'reloadButton',
					value: mw.msg( 'mostedited-reload' ), title: mw.msg( 'mostedited-tooltip-reload' ) } )
			)
		) +
		'</fieldset>';
	html += mw.html.element( 'div', { id: 'mostEditedContainer' }, '' );
	return html;
}

// functions to interact with user

/**
* reads user input from form elements, usable for URL
* @return {object} object with all parameters
*/
function readRawUserInput () {
	return {
		namespace: $( '#namespace option:selected' ).val(),
		invert: $( '#nsinvert' ).prop( 'checked' ),
		associated: $( '#nsassociated' ).prop( 'checked' ),
		hours: $( '#time option:selected' ).val(),
		sort: $( '#mostedited-sort option:selected' ).val(),
		maxCalls: validateNumber( $( '#mostedited-input-max-calls' ), 5 ),
		limit: validateNumber( $( '#mostedited-input-limit' ), 10 ),
		sectionLimit: validateNumber( $( '#mostedited-input-section-limit' ), 3 ),
		edits: validateNumber( $( '#mostedited-input-edits' ), 2 ),
		sectionEdits: validateNumber( $( '#mostedited-input-section-edits' ), 2 )
	};
}

/**
* reads user input from form elements, usable for API
* @return {object} object with all parameters
*/
function readUserInput () {
	var rawInput = readRawUserInput(),
		namespace = rawInput.namespace,
		invert = rawInput.invert,
		associated = rawInput.associated,
		sort = rawInput.sort,
		namespaces,
		hours = rawInput.hours,
		ago = new Date( Date.now() - hours * 3600000 ),
		end = String( ago.getUTCFullYear() ) + '-' +
			pad( ago.getUTCMonth() + 1 ) + '-' +
			pad( ago.getUTCDate() ) + 'T' +
			pad( ago.getUTCHours() ) + ':' +
			pad( ago.getUTCMinutes() ) + ':' +
			pad( ago.getUTCSeconds() ) + 'Z',
		list, formattedNamespaces, i;
	if ( namespace === '' ) {
		namespaces = ''; // all
	} else {
		namespace = Number( namespace );
		list = [ namespace ];
		if ( associated ) {
			list.push(
				( namespace % 2 === 0 ) ?
					namespace + 1 :
					namespace - 1 );
		}
		if ( invert ) {
			namespaces = [];
			formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' );
			for ( i in formattedNamespaces ) {
				if ( i >= 0 && list.indexOf( Number( i ) ) === -1 ) {
					namespaces.push( i );
				}
			}
			namespaces = namespaces.join( '|' );
		} else {
			namespaces = list.join( '|' );
		}
	}
	return {
		namespaces: namespaces,
		sort: sort,
		end: end,
		maxCalls: rawInput.maxCalls,
		limit: rawInput.limit,
		sectionLimit: rawInput.sectionLimit,
		edits: rawInput.edits,
		sectionEdits: rawInput.sectionEdits
	};
}

/**
* called when user clicks reload button
*/
function reloadPage () {
	var params = readRawUserInput();
	document.location.href = mw.util.getUrl( 'Special:BlankPage', {
			action: 'mostedited',
			namespace: params.namespace,
			invert: params.invert ? '1' : '0',
			associated: params.associated ? '1' : '0',
			hours: params.hours,
			sort: params.sort,
			'max-calls': params.maxCalls,
			limit: params.limit,
			'section-limit': params.sectionLimit,
			edits: params.edits,
			'section-edits': params.sectionEdits
		} );
}

/**
* called when user clicks submit button
*/
function submitQuery () {
	var params = readUserInput();
	$( '#submitButton' ).prop( 'disabled', true );
	$( '#mostEditedContainer' ).empty().injectSpinner( 'mostedited' );
	pagesList = {}; // empty
	getAPIRecentChanges( params.end, params.namespaces, params.maxCalls, function ( done ) {
		var $realTime = $( '#mostedited-real-time' ), $div = $( '<div>' );
		if ( done ) {
			firstTime = getTime( params.end );
			$realTime.text( '' );
		} else {
			$realTime.html( mw.html.element( 'span',
				{ title: mw.msg( 'mostedited-changed-period-tooltip' ) },
				mw.msg( 'mostedited-changed-period', formatPeriod( ( currTime - firstTime ) / 3600000 ) ) ) );
		}
		$div.html( generateHTML( params.limit, params.sectionLimit, params.edits, params.sectionEdits, params.sort ) );
		mw.hook( 'wikipage.content' ).fire( $div );
		$( '#mostEditedContainer' ).html( $div );
		$.removeSpinner( 'mostedited' );
		$( '#submitButton' ).prop( 'disabled', false );
	} );
}

// initialise

/**
* initialises the interface on Special:Blankpage
*/
function initBlankpage () {
	document.title = mw.msg( 'pagetitle', mw.msg( 'mostedited' ) );
	var $content = $( '#mw-content-text' ), // don't clear away subtitle, newtalk and jumpto,
		$advancedOptions;
	if ( $content.length !== 1 ) {
		$content = mw.util.$content; // fallback
	}
	$content.html( generateHeaderHTML() );
	// CSS for colors;  enables/disables checkboxes
	mw.loader.load( [ 'mediawiki.special.changeslist', 'mediawiki.special.recentchanges' ] );
	$advancedOptions = $content.find( '#advanced-options' );
	$advancedOptions.makeCollapsible();
	$advancedOptions.find( '.mw-collapsible-toggle' ).appendTo( $advancedOptions );
	$( '#reloadButton' ).on( 'click', reloadPage );
	$( '#submitButton' ).on( 'click', submitQuery ).trigger( 'click' );
}

/**
* initialises the interface on Special:RecentChanges
*/
function initRecentchanges () {
	var $button;
	if ( $( '.rcfilters-head' ).length ) {
		return;
	}
	$button = $( mw.html.element( 'input', { type: 'button', value: mw.msg( 'mostedited-submit' ) } ) )
		.on( 'click', function () {
			var namespace = $( '#namespace option:selected' ).val(),
				invert = $( '#nsinvert' ).prop( 'checked' ) ? '1' : '0',
				associated = $( '#nsassociated' ).prop( 'checked' ) ? '1' : '0';
			document.location.href = mw.util.getUrl( 'Special:BlankPage', {
				action: 'mostedited', namespace: namespace, invert: invert, associated: associated
			} );
		} );
	$( 'input[type="submit"]' ).eq( 0 ).after( $button ); // FIXME breaks when there is another submit button before it
}

/**
* initialises the sidebar everywhere
*/
function initSidebar () {
	var portlet = $( '#n-recentchanges' ).parents( '.portlet, .portal' ).attr( 'id' ) || 'p-navigation';
	mw.util.addPortletLink( portlet,
		mw.util.getUrl( 'Special:BlankPage', { action: 'mostedited' } ),
		mw.msg( 'mostedited' ),
		'n-mostedited',
		mw.msg( 'tooltip-n-mostedited' ),
		null, // access key
		'#n-recentchanges' );
}

mw.loader.using( [ 'mediawiki.language', 'mediawiki.util' ] ).then( function () {
	initL10N( messages, [ 'minutes', 'hours', 'days', 'allpagessubmit', 'namespace',
		'invert', 'tooltip-invert', 'namespace_association', 'tooltip-namespace_association',
		'blanknamespace', 'namespacesall', 'rc-change-size', 'rc-change-size-new', 'pagetitle' ] );

	$( initSidebar );

	if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Recentchanges' ) {
		$( initRecentchanges );
	}

	if (
		mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Blankpage' &&
		mw.util.getParamValue( 'action' ) === 'mostedited'
	) {
		$.when(
			mw.loader.using( [ 'mediawiki.jqueryMsg', 'jquery.spinner', 'jquery.makeCollapsible' ] ),
			$.ready
		).then( initBlankpage );
	}
} );
//virtual outdent
} )( jQuery, mediaWiki );
// </nowiki>