User:Murph9000/usercontribs.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* Enhance Special:Contributions with features from API:Usercontribs.
*
* Copyright © 2017 User:Murph9000 @ English Wikipedia. All rights reserved.
*
* Released under the Creative Commons Attribution-ShareAlike 3.0 Unported License
* Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
*
* Released under the Creative Commons Attribution-ShareAlike 4.0 International License
* Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_4.0_International_License
*
* Released under the GNU Free Documentation License
* Wikipedia:Text_of_the_GNU_Free_Documentation_License
*
* Released under the GNU General Public License, version 2 or later
* https://www.gnu.org/licenses/gpl-2.0.html
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/*
* Copyright (c) 1990, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
( function ( mw, $ ) {
'use strict';
var FILE = 'meta:User:Murph9000/usercontribs.js';
console.info( FILE, 'startup' );
var $content, $form, form, $inputContinue, $list, $spinner,
$usersDl, $usersDt, $usersDd, $usersList,
Api, cachingApi, oresPromise, specialPageAliasesPromise, tagsPromise,
usercontribsContinue,
conf = mw.config.get( [
'wgFormattedNamespaces',
'wgNamespaceIds',
'wgUserName',
] );
// This isn't exported to the JS env for some reason
// 500 is the MW 1.28 default setting
conf.wgRCChangedSizeThreshold = 500;
var NS_USER = conf.wgNamespaceIds.user,
NS_USER_TALK = conf.wgNamespaceIds.user_talk,
contributionsTitle = 'Special:Contributions',
nsUser = conf.wgFormattedNamespaces[ NS_USER ],
nsUserTalk = conf.wgFormattedNamespaces[ NS_USER_TALK ];
/**
* mediawiki-1.28.2/includes/templates/SpecialContributionsLine.mustache
*/
var templateName = 'SpecialContributionsLine.mustache',
templateBody = '\
{{{ del }}}{{{ timestamp }}}\n\
{{{ diffHistLinks }}}{{{ charDifference }}}{{# flags }}{{{ . }}}{{/ flags }}\n\
{{{ articleLink }}}{{{ userlink }}}\n\
{{{ logText }}}\n\
{{{ topmarktext }}}{{# rev-deleted-user-contribs }} <strong>{{{ . }}}</strong>{{/ rev-deleted-user-contribs }}\n\
{{{ tagSummary }}}\n\
';
var Html = mw.html,
mLimitsShown = [ 10, 20, 50, 100, 250, 500 ],
mDefaultLimit = 50,
ores = {
damagingPrefMap: {
'hard': 'maybebad',
'soft': 'likelybad',
'softest': 'verylikelybad',
},
highlight: mw.user.options.get( 'oresHighlight' ),
damagingPref: mw.user.options.get( 'oresDamagingPref' ),
thresholds: mw.config.get( 'oresThresholds' ),
// Order must be highest to lowest severity,
// to match processing in usercontribsCallback()
levels: [ 'verylikelybad', 'likelybad', 'maybebad' ],
},
param = {},
ucParam = {},
ucprop = [
'ids',
'title',
'timestamp',
'parsedcomment',
'size',
'sizediff',
'flags',
'tags',
],
users = {},
separator = ' <span class="mw-changeslist-separator">. .</span> ';
// @(#)bsearch.c 8.1 (Berkeley) 6/4/93
// $FreeBSD: releng/10.3/lib/libc/stdlib/bsearch.c 251069 2013-05-28 20:57:40Z emaste $
/*
* Perform a binary search.
*
* The code below is a bit sneaky. After a comparison fails, we
* divide the work in half by moving either left or right. If lim
* is odd, moving left simply involves halving lim: e.g., when lim
* is 5 we look at item 2, so we change lim to 2 so that we will
* look at items 0 & 1. If lim is even, the same applies. If lim
* is odd, moving right again involes halving lim, this time moving
* the base up one item past p: e.g., when lim is 5 we change base
* to item 3 and make lim 2 so that we will look at items 3 and 4.
* If lim is even, however, we have to shrink it by one before
* halving: e.g., when lim is 4, we still looked at item 2, so we
* have to make lim 3, then halve, obtaining 1, so that we will only
* look at item 3.
*/
function bsearch(key, arr, compar)
{
var base = 0;
var lim;
var cmp;
var p;
for (lim = arr.length; lim !== 0; lim >>= 1) {
p = base + (lim >> 1);
cmp = compar(key, arr[p]);
if (cmp === 0)
return arr[p];
if (cmp > 0) { /* key > p: move right */
base = p + 1;
lim--;
} /* else move left */
}
return null;
}
function compare( left, right ) {
return ((left > right) - (left < right));
}
/**
* Given a value, escape it so that it can be used as a CSS class and
* return it.
*
* @see http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
*
* mediawiki-1.28.2/includes/Sanitizer.php:escapeClass()
*
* @param string $class
* @return string
*/
function escapeClass( $class ) {
// Convert ugly stuff to underscores and kill underscores in ugly places
return $class
.replace( /(^[0-9\-])|[\x00-\x20!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~]|\xC2\xA0/g, '_' )
.replace( /_+/g, '_' )
.replace( /_$/, '' );
}
/**
* extend mw.Api
*/
function extendApi( Api ) {
$.extend( Api.prototype, {
getOres: function () {
return this.get( {
action: 'query',
meta: 'ores'
} ).then( function ( data ) {
console.log( FILE, 'getOres data', data );
if ( !data.batchcomplete ) {
mw.log.warn( FILE, 'specialpagealiases API query, batchcomplete not true');
}
if ( data.query && data.query.ores ) {
return data.query.ores;
}
return $.Deferred().reject().promise();
} );
},
loadOres: function () {
return this.getOres().then( function ( data ) {
ores.thresholds = {
damaging: data.damagingthresholds
};
} );
},
loadOresIfMissing: function () {
if ( !ores.highlight || ores.thresholds ) {
return $.Deferred().resolve();
}
return this.loadOres();
},
getSpecialPageAliases: function () {
return this.get( {
action: 'query',
meta: 'siteinfo',
siprop: 'specialpagealiases',
} ).then( function ( data ) {
console.log( FILE, 'getSpecialPageAliases data', data );
if ( !data.batchcomplete ) {
mw.log.warn( FILE, 'specialpagealiases API query, batchcomplete not true');
}
if ( data.query && data.query.specialpagealiases ) {
return data.query.specialpagealiases;
}
return $.Deferred().reject().promise();
} );
},
loadSpecialPageAliases: function () {
return this.getSpecialPageAliases().then(
function ( specialpagealiases ) {
var result = {};
for ( var page of specialpagealiases ) {
result[ page.realname ] = page.aliases;
}
return result;
}
);
},
getTags: function ( options ) {
options = options || {};
return this.get( $.extend( {
action: 'query',
list: 'tags',
tglimit: 'max',
tgprop: [ 'name', 'displayname' ],
}, options ) ).then( function ( data ) {
var result = {};
console.log( FILE, 'getTags data', data );
if ( !data.batchcomplete ) {
mw.log.warn( FILE, 'tags API query, batchcomplete not true');
}
for ( var tag of data.query.tags ) {
result[ 'tag-' + tag.name ] = tag.displayname;
}
return result;
} );
},
loadTags: function ( options ) {
return this.getTags( options ).then( $.proxy( mw.messages, 'set' ) );
},
getUserContribs: function ( options ) {
console.log( 'getUserContribs', options );
options = options || { ucuser: conf.wgUserName };
return this.get( $.extend( {
action: 'query',
list: 'usercontribs',
ucprop: ucprop,
}, options ) );
}
} );
}
/**
* extend mw.Message
*/
function extendMessage( Message ) {
if ( Message.prototype.isDisabled ) {
// Function exists, possibly added to MW
return Message;
}
return $.extend( Message.prototype, {
/**
* Check if a message is disabled
*
* @return {boolean}
*/
isDisabled: function () {
var message = this.map.get( this.key );
return message === null ||
message === false ||
message === '' ||
message === '-';
}
} );
}
/**
* extend mw.Title
*/
function extendTitle( Title ) {
return $.extend( Title.exist, {
batchLimit: 50,
batchQueue: [],
batchRunning: [],
batchQuery: function ( title ) {
var limit = this.batchLimit,
queue = this.batchQueue,
running = this.batchRunning;
if ( queue.includes( title ) ||
running.includes( title ) ) {
return;
}
queue.push( title );
if ( queue.length >= limit ) {
this.batchRun();
}
},
batchRun: function () {
var titles,
limit = this.batchLimit,
queue = this.batchQueue,
running = this.batchRunning;
if (queue.length) {
titles = queue.splice(0, limit);
running = running.concat( titles );
this.query( titles, this.batchRunCallback.bind( this ) );
}
},
batchRunCallback: function () {
if ( this.batchQueue.length >= this.batchLimit ) {
this.batchRun();
}
},
hook: mw.hook( 'mediawiki.Title.exist' ),
query: function ( titles, callback ) {
return Api.get( {
action: 'query',
prop: 'info',
titles: titles,
} ).done( callback, this.queryCallback.bind( this ) );
},
queryCallback: function ( data ) {
console.log('Title.exist.queryCallback', this, data);
var hook = this.hook,
pages = this.pages,
queryPages = data.query.pages,
running = this.batchRunning;
for ( var i = 0, len = queryPages.length; i < len; i++ ) {
var index,
page = queryPages[i],
title = page.title,
state = !page.missing;
//this.set( title, state );
pages[ title ] = state;
hook.fire( title );
running.indexOf( title );
if ( index >= 0 ) {
running.splice( index, 1 );
}
}
},
} );
}
function titleExistsHandler( title ) {
if ( mw.Title.exists( title ) === false ) {
$content.find( 'a[title="' + title.replace( /_/g, ' ' ) + '"]' )
.addClass( 'new' );
}
}
/**
* Make user link (or user contributions for unregistered users)
*
* mediawiki-1.28.2/includes/Linker.php
*
* @param int $userId User id in database.
* @param string $userName User name in database.
* @param string $altUserName Text to display instead of the user name (optional)
* @return string HTML fragment
*/
function userLink( userId, userName, altUserName ) {
var page,
classes = 'mw-userlink';
if ( userId === 0 ) {
page = mw.Title.newFromText( contributionsTitle + '/' + userName );
// PHP does a $altUserName = IP::prettifyIP( $userName );
classes += ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
} else {
page = mw.Title.makeTitle( NS_USER, userName );
switch ( page.exists() ) {
case null: mw.Title.exist.batchQuery( page.toString() ); break;
case false: classes = 'new ' + classes;
}
}
// Wrap the output with <bdi> tags for directionality isolation
return Html.element( 'a', {
href: page.getUrl(),
class: classes,
title: page.getPrefixedText()
}, new Html.Raw(
'<bdi>' +
Html.escape( altUserName !== undefined ? altUserName : userName ) +
'</bdi>'
) );
}
function userToolLinks( userId, userText ) {
var items, page;
items = [];
items.push( userTalkLink( userId, userText ) );
if ( userId ) {
page = mw.Title.newFromText( contributionsTitle + '/' + userText );
items.push( Html.element( 'a', {
href: page.getUrl(),
class: 'mw-usertoollinks-contribs',
title: page.getPrefixedText()
}, mw.msg( 'contribslink' ) )
);
}
return mw.msg( 'word-separator' )
+ '<span class="mw-usertoollinks">'
+ mw.message( 'parentheses',
items.join( mw.msg( 'pipe-separator' ) )
).text()
+ '</span>';
}
/**
* @param int $userId User id in database.
* @param string $userText User name in database.
* @return string HTML fragment with user talk link
*/
function userTalkLink( userId, userText ) {
var page = mw.Title.makeTitle( NS_USER_TALK, userText ),
classes = 'mw-usertoollinks-talk';
switch ( page.exists() ) {
case null: mw.Title.exist.batchQuery( page.toString() ); break;
case false: classes = 'new ' + classes;
}
return Html.element( 'a', {
href: page.getUrl(),
class: classes,
title: page.getPrefixedText()
}, mw.msg( 'talkpagelinktext' ) );
}
/**
* Format the character difference of the change.
*
* Converted from mediawiki-1.28.2/includes/changes/ChangesList.php
* showCharacterDifference()
* formatCharacterDifference()
*
* @param {Object} item A single change item from the usercontribs API
* @return string HTML fragment
*/
function formatCharacterDifference( item ) {
var formattedSize, formattedSizeClass, formattedTotalSize, tag,
szdiff = item.sizediff;
formattedSize = mw.language.convertNumber( szdiff );
formattedSize = mw.msg( 'rc-change-size', formattedSize );
if ( Math.abs( szdiff ) > conf.wgRCChangedSizeThreshold ) {
tag = 'strong';
} else {
tag = 'span';
}
if ( szdiff === 0 ) {
formattedSizeClass = 'mw-plusminus-null';
} else if ( szdiff > 0 ) {
formattedSize = '+' + formattedSize;
formattedSizeClass = 'mw-plusminus-pos';
} else {
formattedSizeClass = 'mw-plusminus-neg';
}
formattedTotalSize = mw.msg( 'rc-change-size-new', item.size );
return Html.element( tag,
{ class: formattedSizeClass, title: formattedTotalSize },
mw.msg( 'parentheses', formattedSize ) );
}
/**
* mediawiki-1.28.2/includes/changetags/ChangeTags.php:formatSummaryRow()
*/
function formatTags( tags ) {
var classes, description, displayTags, markers;
if ( !tags.length ) {
return [ '', [] ];
}
classes = [];
displayTags = [];
for ( var tag of tags ) {
if ( !tag ) {
continue;
}
description = tagDescription( tag );
if ( description === false ) {
continue;
}
displayTags.push( Html.element( 'span', {
class: 'mw-tag-marker ' +
escapeClass( 'mw-tag-marker-' + tag )
}, new Html.Raw( description ) ) );
classes.push( escapeClass( 'mw-tag-' + tag ) );
}
if ( !displayTags.length ) {
return [ '', [] ];
}
// This should work, but it looks like mediawiki.jqueryMsg
// is unable to cope with it
// TODO: investigate parser settings
/*markers = mw.message( 'tag-list-wrapper',
displayTags.length,
displayTags.join(', ')
).parse();*/
markers = mw.message( 'tag-list-wrapper', displayTags.length ).parse();
markers = markers.replace( /\$2/g,
displayTags.join( mw.msg( 'comma-separator' ) ) );
markers = Html.element( 'span', { class: 'mw-tag-markers' },
new Html.Raw( markers ) );
return [ markers, classes ];
}
/**
* Get a short description for a tag.
*
* Checks if message key "mediawiki:tag-$tag" exists. If it does not,
* returns the HTML-escaped tag name. Uses the message if the message
* exists, provided it is not disabled. If the message is disabled,
* we consider the tag hidden, and return false.
*
* mediawiki-1.28.2/includes/changetags/ChangeTags.php
*
* @param {string} tag Tag
* @return {string|boolean} Tag description or false if tag is to be hidden.
*/
function tagDescription( tag ) {
var msg = mw.message( 'tag-' + tag );
if ( !msg.exists() ) {
// No such message, so return the HTML-escaped tag name.
return Html.escape( tag );
}
if ( msg.isDisabled() ) {
// The message exists but is disabled, hide the tag.
return false;
}
// Message exists and isn't disabled, use it.
return msg.text();
}
/**
* Generates each row in the contributions list.
*
* Contributions which are marked "top" are currently on top of the history.
* For these contributions, a [rollback] link is shown for users with roll-
* back privileges. The rollback link restores the most recent version that
* was not written by the target user.
*
* mediawiki-1.28.2/includes/specials/pagers/ContribsPager.php
*
* @param {Object} row
* @return {jQuery} HTML list item
*/
function formatRow( row ) {
var chardiff, classes, comment, d, del, diffHistLinks, difflink,
flags, histlink, link, newClasses, $ret, tagSummary,
templateParams, topmarktext, userlink;
classes = [];
link = Html.element( 'a', {
href: mw.util.getUrl( row.title ),
class: 'mw-contributions-title',
title: row.title
}, row.title );
// Mark current revisions
topmarktext = '';
if (row.top) {
topmarktext += '<span class="mw-uctop">' + mw.msg('uctop') + '</span>';
classes.push( 'mw-contributions-current' );
// TODO: Add rollback link
}
// TODO: Is there a visible previous revision?
difflink = Html.element( 'a', {
href: mw.util.getUrl( row.title, {
diff: 'prev',
oldid: row.revid
} ),
class: 'mw-changeslist-diff',
title: row.title
}, mw.msg( 'diff' ) );
histlink = Html.element( 'a', {
href: mw.util.getUrl( row.title, { action: 'history' } ),
class: 'mw-changeslist-history',
title: row.title
}, mw.msg( 'hist' ) );
chardiff = separator;
chardiff += formatCharacterDifference( row );
chardiff += separator;
comment = row.parsedcomment;
if (comment) {
comment = '<span class="comment">' +
mw.msg( 'parentheses', comment ) +
'</span>';
}
d = Html.element( 'a', {
href: mw.util.getUrl( row.title, { oldid: row.revid } ),
class: 'mw-changeslist-date',
title: row.title
}, row.timestamp );
// Show user names for /newbies as there may be different users.
// Note that we already excluded rows with hidden user names.
if ( param.contribs === 'newbie' || param.contribs === 'userprefix' ) {
userlink = separator
+ userLink( row.userid, row.user );
/*userlink += ' ' + mw.message( 'parentheses',
userTalkLink( row.userid, row.user )
).text() + ' ';*/
userlink += ' ' + userToolLinks( row.userid, row.user ) + ' ';
} else {
userlink = '';
}
flags = [];
// TODO: new
// TODO: minor
del = ''; // TODO: getRevDeleteLink;
diffHistLinks = mw.message( 'parentheses',
difflink + mw.msg( 'pipe-separator' ) + histlink
).text();
[ tagSummary, newClasses ] = formatTags( row.tags );
classes = classes.concat( newClasses );
templateParams = {
del: del,
timestamp: d,
diffHistLinks: diffHistLinks,
charDifference: chardiff,
flags: flags,
articleLink: link,
userlink: userlink,
logText: comment,
topmarktext: topmarktext,
tagSummary: tagSummary,
};
$ret = mw.template.get( 'mediawiki.special', templateName )
.render( templateParams );
if ( classes === [] && ret === '' ) {
mw.log( 'Dropping Special:Contribution row that could not be formatted' );
return '<!-- Could not format Special:Contribution row. -->\n';
}
// mediawiki.template.mustache renders to a jQuery object
// Just going with that for now as it creates the DOM nodes we need
//return Html.element( 'li', { class: classes }, new Html.Raw( ret ) );
return $( '<li>' ).addClass( classes.join(' ') ).append( $ret );
}
function usercontribsCallback( data ) {
console.log( FILE + ':usercontribsCallback():', data );
var rev, oresDamaging, oresGoodfaith, oresLevel, key, li, ref,
usersCount,
oresDamagingThresholds,
usercontribs = data.query.usercontribs,
list = $list[0],
listNodes = list.childNodes; // Live NodeList
/* Although the auto-incrementing revid seems like a perfectly
* reasonable sort key, it turns out that it has been reset in some
* databases, so does not always represent a chronology.
* German Wikipedia is one example of this, for unknown reasons.
*
* mediawiki-1.28.2/includes/specials/pagers/ContribsPager.php
* getIndexField() uses 'rev_timestamp'
*/
/*function compareNodes( key, node ) {
var revid = Number( node.dataset.revid ),
previousSibling = node.previousSibling,
left = previousSibling ?
Number( previousSibling.dataset.revid ) : Infinity;
return compare( revid, key ) - compare( key, left );
}*/
function compareNodes( key, node ) {
var timestamp = new Date( node.dataset.timestamp ),
previousSibling = node.previousSibling,
left = previousSibling ?
new Date( previousSibling.dataset.timestamp ) : Infinity;
return compare( timestamp, key ) - compare( key, left );
}
if ( !data.batchcomplete ) {
mw.log.warn( FILE, 'usercontribs API query, batchcomplete not true');
}
if ( ores.highlight ) {
oresDamagingThresholds = ores.thresholds.damaging;
}
for ( var i = 0, len = usercontribs.length; i < len; i++ ) {
rev = usercontribs[i];
li = formatRow( rev )[0];
li.dataset.revid = rev.revid;
li.dataset.timestamp = rev.timestamp;
li.dataset.sizediff = rev.sizediff;
li.dataset.title = rev.title;
li.dataset.user = rev.user;
if ( ores.highlight && rev.oresscores ) {
oresDamaging = rev.oresscores.damaging.true;
oresGoodfaith = rev.oresscores.goodfaith.true;
li.dataset.oresDamaging = oresDamaging;
li.dataset.oresGoodfaith = oresGoodfaith;
// Intentionally fall off the end of the array here,
// to get undefined where the score is below the lowest level
for ( var j = 0, jLen = ores.levels.length; j <= jLen; j++ ) {
oresLevel = ores.levels[j];
if ( oresDamaging > oresDamagingThresholds[ oresLevel ] ) {
break;
}
}
if ( oresLevel ) {
li.classList.add(
'ores-highlight', 'ores-damaging-' + oresLevel
);
}
}
//key = Number( li.dataset.revid );
key = new Date( li.dataset.timestamp );
ref = bsearch( key, listNodes, compareNodes );
list.insertBefore( li, ref );
if ( users[ rev.user ] ) {
users[ rev.user ].count++;
} else {
users[ rev.user ] = { id: rev.userid, count: 1 };
}
}
console.log( FILE, i + ' revisions loaded' );
$usersList.empty();
usersCount = 0;
for ( var user in users ) {
var rec = users[ user ];
$usersList.append(
$( '<li>' ).html(
userLink( rec.id, user ) +
userToolLinks( rec.id, user ) +
rec.count
)
);
usersCount++;
}
$usersDt.text( usersCount + ' users found' );
mw.Title.exist.batchRun();
$spinner.remove();
if ( data.continue ) {
usercontribsContinue = data.continue;
$inputContinue.prop( 'disabled', false );
} else {
$inputContinue.prop( 'disabled', true );
}
}
function continueHandler( event ) {
console.log( FILE + ':continueHandler():', event );
$inputContinue.prop( 'disabled', true );
$spinner = $.createSpinner( { size: 'small', type: 'inline' } );
$inputContinue.after( $spinner );
Api.getUserContribs( $.extend( ucParam, usercontribsContinue ) )
.done( usercontribsCallback );
}
function submitHandler( event ) {
console.log( FILE + ':submitHandler():', event );
var ucshow, namespaces, ns, options;
param = {};
$.each( [ 'contribs', 'limit', 'prefix' ], function ( i, name ) {
param[ name ] = form.elements[ name ].value;
} );
if ( param.contribs === 'userprefix' ) {
event.preventDefault();
if ( !param.prefix ) {
// TODO: communicate with the user
return;
}
$.each( [
'limit',
'namespace',
'tagfilter'
], function ( i, name ) {
param[ name ] = form.elements[ name ].value;
} );
$.each( [
'nsInvert', 'associated',
'topOnly', 'newOnly', 'hideMinor', 'hidenondamaging'
], function ( i , name ) {
if ( form.elements[ name ].checked ) {
param[ name ] = form.elements[ name ].value;
}
} );
ucParam = {};
ucParam.uclimit = param.limit;
ucParam.ucuserprefix = param.prefix;
if ( param.namespace ) {
if ( param.associated ) {
ns = Number( param.namespace );
if ( ns % 2 ) {
ucParam.ucnamespace = [ String( ns - 1 ), String( ns ) ];
} else {
ucParam.ucnamespace = [ String( ns ), String( ns + 1 ) ];
}
} else {
ucParam.ucnamespace = [ param.namespace ];
}
if ( param.nsInvert ) {
namespaces = [];
options = form.elements.namespace.options;
for ( var i = 0, len = options.length; i < len; i++ ) {
ns = options[i].value;
if ( ns && !ucParam.ucnamespace.includes( ns ) ) {
namespaces.push( ns );
}
}
ucParam.ucnamespace = namespaces;
}
} // TODO: associated & inverted
ucshow = [];
if ( param.topOnly ) {
ucshow.push( 'top' );
}
if ( param.newOnly ) {
ucshow.push( 'new' );
}
if ( param.hideMinor ) {
ucshow.push( '!minor' );
}
if ( param.hidenondamaging ) {
ucshow.push( 'oresreview' );
}
if ( ucshow.length ) {
ucParam.ucshow = ucshow;
}
if ( param.tagfilter ) {
ucParam.uctag = param.tagfilter;
}
$usersDl = $usersDt = $usersDd = $usersList = null;
$content.children().remove( 'form ~ *' );
$spinner = $.createSpinner( { size: 'large', type: 'block' } );
$content.append( $spinner );
users = {};
$usersDl = $( '<dl>' ).append( [
$usersDt = $( '<dt>' ),
$usersDd = $( '<dd>' ).append(
$usersList = $( '<ul>' )
).makeCollapsible( {
collapsed: true,
$customTogglers: $usersDt
} )
] );
$content.append( $usersDl );
$list = $( '<ul>' ).attr( 'class', 'mw-contributions-list' );
$content.append( $list );
mw.Title.exist.hook.add( titleExistsHandler );
$.when(
Api.getUserContribs( ucParam ),
oresPromise,
specialPageAliasesPromise,
tagsPromise
).done( function ( data ) {
// $.when( … ).done() produces a variation over a simple .done()
usercontribsCallback.apply( this, data );
} );
}
}
function loaderCallback() {
console.log( FILE, 'loader done' );
extendApi( mw.Api );
extendMessage( mw.Message );
extendTitle( mw.Title );
Api = new mw.Api( {
parameters: {
formatversion: 2
}
} );
cachingApi = new mw.Api( {
parameters: {
formatversion: 2,
smaxage: 86400,
maxage: 86400
}
} );
return $.when(
/* Messages are needed early, but other API queries are
* not needed until after submit.
*/
cachingApi.loadMessagesIfMissing( [
'boteditletter',
'collapsible-collapse',
'collapsible-expand',
'comma-separator',
'contribslink',
'contributions-title',
'diff',
'hist',
'log',
'minoreditletter',
'newpageletter',
'ores-damaging-letter',
'ores-damaging-title',
'pagetitle',
'parentheses',
'pipe-separator',
'rc-change-size',
'rc-change-size-new',
'recentchanges-label-bot',
'recentchanges-label-minor',
'recentchanges-label-newpage',
'sp-contributions-username',
//'sp-contributions-username-prefix', // Not yet available
'tag-list-wrapper',
'talkpagelinktext',
'uctop',
'viewpagelogs',
'word-separator',
] ),
$.ready
);
}
function initPage() {
var $fieldset;
console.info( FILE, 'ready' );
if ( !mw.messages.exists( 'sp-contributions-username-prefix' ) ) {
mw.messages.set( 'sp-contributions-username-prefix',
mw.messages.get( 'sp-contributions-username' )
.replace( /:?\s*$/, ' prefix$&' )
);
}
$content = $( '#mw-content-text' );
// Extend form as early as possible for better user experience.
$form = $content.find( 'form.mw-contributions-form' );
form = $form[0];
$fieldset = $form.find( 'fieldset.mw-contributions-table' );
// MW adds a hidden limit field, remove it to use our selector
$form.find( 'input[name="limit"]' ).remove();
$fieldset.find( 'input[name="target"]' ).after( [
'<br>',
Html.element( 'input', {
name: 'contribs',
type: 'radio',
value: 'userprefix',
id: 'userprefix',
class: 'mw-input'
} ),
' ',
Html.element( 'label', {
for: 'userprefix',
class: 'mw-input'
}, mw.msg( 'sp-contributions-username-prefix' ) ),
' ',
Html.element( 'input', {
size: '40',
class: 'mw-input mw-ui-input-inline mw-autocomplete-user',
name: 'prefix'
} ),
' '
] );
$fieldset.find( '> div:last' ).before(
Html.element( 'div', {}, new Html.Raw(
Html.element( 'label', { for: 'limit' }, 'Limit:') +
' ' +
Html.element( 'select', { id: 'limit', name: 'limit' }, new Html.Raw(
mLimitsShown.reduce( function ( acc, val ) {
return acc + Html.element( 'option', {}, val );
}, '')
) )
) )
).append(
$inputContinue = $( '<input>' ).attr( {
class: 'mw-button',
type: 'button',
value: 'Continue'
} ).prop( 'disabled', true )
);
$form.on( 'submit', submitHandler );
$inputContinue.on( 'click', continueHandler );
console.log( FILE, 'form extended' );
}
function pageReady() {
$.each( [ 'contribs', 'limit', 'prefix' ], function ( i, name ) {
var value = mw.util.getParamValue ( name );
param[name] = value;
if ( value ) {
form.elements[ name ].value = value;
}
} );
if ( !param.limit ) {
param.limit = mw.user.options.get( 'rclimit' );
form.elements.limit.value = param.limit;
}
if ( ores.highlight ) {
ucprop.push( 'oresscores' );
ores.style = mw.util.addCSS(
'.ores-damaging-verylikelybad { background-color: #fc3; } ' +
'.ores-damaging-likelybad { background-color: #ffe79e; } ' +
'.ores-damaging-maybebad { background-color: #fef6e7; }'
);
ores.levels.splice( ores.levels.indexOf(
ores.damagingPrefMap[ ores.damagingPref ]
) + 1);
}
console.log( 'ORES', ores );
oresPromise = cachingApi.loadOresIfMissing();
specialPageAliasesPromise = cachingApi.loadSpecialPageAliases()
.done( function ( specialPageAliases ) {
var aliases = specialPageAliases.Contributions,
specialNamespace = conf.wgFormattedNamespaces[
conf.wgNamespaceIds.special
];
if ( aliases && aliases.length ) {
contributionsTitle = specialNamespace + ':' + aliases[0];
}
}
);
tagsPromise = cachingApi.loadTags();
mw.template.add( 'mediawiki.special', templateName, templateBody );
if ( param.contribs === 'userprefix' && param.prefix ) {
$form.submit();
}
window.ucparam = param;
console.log( FILE, 'startup complete' );
}
if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Contributions' ) {
mw.loader.using( [
'mediawiki.Title',
'mediawiki.api',
'mediawiki.api.messages',
'mediawiki.jqueryMsg',
'mediawiki.language',
'mediawiki.template',
'mediawiki.template.mustache',
'mediawiki.user',
'mediawiki.util',
'jquery.makeCollapsible',
'jquery.spinner'
] ).then( loaderCallback ).then( initPage ).done( pageReady );
}
} )( mediaWiki, jQuery );