MediaWiki:Gadget-morebits.js: Difference between revisions

m
1 revision imported
(Good version)
 
m (1 revision imported)
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
// <nowiki>
//<nowiki>
/**
/**
  * morebits.js
  * morebits.js
Line 20: Line 20:
  *    For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy].
  *    For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy].
  *  - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
  *  - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
  *      * GadgetName[ResourceLoader|dependencies=mediawiki.util,jquery.ui.dialog,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js
  *      * GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.RegExp,jquery.ui.dialog,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js
  *
  *
  * Most of the stuff here doesn't work on IE < 9.  It is your script's responsibility to enforce this.
  * Most of the stuff here doesn't work on IE < 9.  It is your script's responsibility to enforce this.
Line 43: Line 43:


Morebits.userIsInGroup = function ( group ) {
Morebits.userIsInGroup = function ( group ) {
return $.inArray(group, mw.config.get( 'wgUserGroups' )) !== -1;
return mw.config.get( 'wgUserGroups' ).indexOf( group ) !== -1;
};
};


Line 49: Line 49:


/**
/**
  * **************** Morebits.isIPAddress() ****************
  * **************** Morebits.sanitizeIPv6() ****************
  * Helper function: Returns true if given string contains a valid IPv4 or
  * JavaScript translation of the MediaWiki core function IP::sanitizeIP() in
  * IPv6 address
* includes/utils/IP.php.
  * Converts an IPv6 address to the canonical form stored and used by MediaWiki.
  */
  */


Morebits.isIPAddress = function ( address ) {
Morebits.sanitizeIPv6 = function ( address ) {
return mw.util.isIPv4Address(address) || mw.util.isIPv6Address(address);
address = address.trim();
if ( address === '' ) {
return null;
}
if ( !mw.util.isIPv6Address( address ) ) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
var abbrevPos = address.indexOf( '::' );
if ( abbrevPos > -1 ) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
var CIDRStart = address.indexOf( '/' );
var addressEnd = ( CIDRStart > -1 ) ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
var repeat, extra, pad;
if ( abbrevPos === 0 ) {
repeat = '0:';
extra = ( address == '::' ) ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if ( abbrevPos === ( addressEnd - 1 ) ) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
var replacement = repeat;
pad -= address.split( ':' ).length - 1;
for ( var i = 1; i < pad; i++ ) {
replacement += repeat;
}
replacement += extra;
address = address.replace( '::', replacement );
}
// Remove leading zeros from each bloc as needed
address = address.replace( /(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2' );
 
return address;
};
};


Line 62: Line 107:
/**
/**
  * **************** Morebits.quickForm ****************
  * **************** Morebits.quickForm ****************
  * Morebits.quickForm is a class for creation of simple and standard forms without much  
  * Morebits.quickForm is a class for creation of simple and standard forms without much
  * specific coding.
  * specific coding.
  *
  *
Line 74: Line 119:
  *              - Attributes: label, list
  *              - Attributes: label, list
  *  field    A fieldset (aka group box).
  *  field    A fieldset (aka group box).
  *              - Attributes: name, label
  *              - Attributes: name, label, disabled
  *  checkbox  A checkbox. Must use "list" parameter.
  *  checkbox  A checkbox. Must use "list" parameter.
  *              - Attributes: name, list, event
  *              - Attributes: name, list, event
Line 97: Line 142:
  *  textarea  A big, multi-line text box.
  *  textarea  A big, multi-line text box.
  *              - Attributes: name, label, value, cols, rows, disabled, readonly
  *              - Attributes: name, label, value, cols, rows, disabled, readonly
*  fragment  A DocumentFragment object.
*              - No attributes, and no global attributes except adminonly
  *
  *
  * Global attributes: id, style, tooltip, extra, adminonly
  * Global attributes: id, className, style, tooltip, extra, adminonly
  */
  */


Line 165: Line 212:
}
}
break;
break;
case 'fragment':
node = document.createDocumentFragment();
// fragments can't have any attributes, so just return it straight away
return [ node, node ];
case 'select':
case 'select':
node = document.createElement( 'div' );
node = document.createElement( 'div' );
Line 237: Line 288:
if( data.name ) {
if( data.name ) {
node.setAttribute( 'name', data.name );
node.setAttribute( 'name', data.name );
}
if( data.disabled ) {
node.setAttribute( 'disabled', 'disabled' );
}
}
break;
break;
Line 264: Line 318:
subnode.setAttribute( 'id', cur_id );
subnode.setAttribute( 'id', cur_id );


if( current.checked )  
if( current.checked ) {
{
subnode.setAttribute( 'checked', 'checked' );
subnode.setAttribute( 'checked', 'checked' );
}
}
Line 276: Line 329:
if( current.tooltip ) {
if( current.tooltip ) {
Morebits.quickForm.element.generateTooltip( label, current );
Morebits.quickForm.element.generateTooltip( label, current );
}
// styles go on the label, doesn't make sense to style a checkbox/radio
if( current.style ) {
label.setAttribute( 'style', current.style );
}
}


Line 282: Line 339:
var tmpgroup = current.subgroup;
var tmpgroup = current.subgroup;


if( ! $.isArray( tmpgroup ) ) {
if( ! Array.isArray( tmpgroup ) ) {
tmpgroup = [ tmpgroup ];
tmpgroup = [ tmpgroup ];
}
}
Line 392: Line 449:
disabled: min >= max,
disabled: min >= max,
event: function(e) {
event: function(e) {
var area = e.target.area;
var new_node =  new Morebits.quickForm.element( e.target.sublist );
var new_node =  new Morebits.quickForm.element( e.target.sublist );
e.target.area.appendChild( new_node.render() );
e.target.area.appendChild( new_node.render() );
Line 493: Line 549:
}
}
if (data.label) {
if (data.label) {
if ( ! $.isArray( data.label ) ) {
if ( ! Array.isArray( data.label ) ) {
data.label = [ data.label ];
data.label = [ data.label ];
}
}
Line 578: Line 634:
if( data.style ) {
if( data.style ) {
childContainder.setAttribute( 'style', data.style );
childContainder.setAttribute( 'style', data.style );
}
if( data.className ) {
childContainder.className = ( childContainder.className ?
childContainder.className + " " + data.className :
data.className );
}
}
childContainder.setAttribute( 'id', data.id || id );
childContainder.setAttribute( 'id', data.id || id );


return [ node, childContainder ];
return [ node, childContainder ];
};
Morebits.quickForm.element.autoNWSW = function() {
return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 'sw' : 'nw';
};
};


Line 590: Line 655:
'fallback': data.tooltip,
'fallback': data.tooltip,
'fade': true,
'fade': true,
'gravity': $.fn.tipsy.autoWE,
'gravity': (data.type === "input" || data.type === "select") ?
Morebits.quickForm.element.autoNWSW : $.fn.tipsy.autoWE,
'html': true,
'html': true,
'delayOut': 250
'delayOut': 250
Line 684: Line 750:
return element.parentNode.getElementsByTagName("label")[0];
return element.parentNode.getElementsByTagName("label")[0];
}
}
return null;
};
};


Line 734: Line 798:
  * **************** HTMLFormElement ****************
  * **************** HTMLFormElement ****************
  *
  *
  * getChecked:  
  * getChecked:
  *  XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better
  *  XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better
  *
  *
Line 796: Line 860:


RegExp.escape = function( text, space_fix ) {
RegExp.escape = function( text, space_fix ) {
text = $.escapeRE(text);
text = mw.RegExp.escape(text);


// Special MediaWiki escape - underscore/space are often equivalent
// Special MediaWiki escape - underscore/space are often equivalent
Line 916: Line 980:
}
}


// Helper functions to change case of a string
Morebits.string = {
Morebits.string = {
// Helper functions to change case of a string
toUpperCaseFirstChar: function(str) {
toUpperCaseFirstChar: function(str) {
str = str.toString();
str = str.toString();
Line 933: Line 997:
var initial = null;
var initial = null;
var result = [];
var result = [];
if( ! $.isArray( skip ) ) {
if( ! Array.isArray( skip ) ) {
if( skip === undefined ) {
if( skip === undefined ) {
skip = [];
skip = [];
Line 969: Line 1,033:
// for deletion/other templates taking a freeform "reason" from a textarea (e.g. PROD, XFD, RPP)
// for deletion/other templates taking a freeform "reason" from a textarea (e.g. PROD, XFD, RPP)
formatReasonText: function( str ) {
formatReasonText: function( str ) {
return str.toString().trimRight().replace(/\|/g, "{{subst:!}}");
var result = str.toString().trimRight();
var unbinder = new Morebits.unbinder(result);
unbinder.unbind("<no" + "wiki>", "</no" + "wiki>");
unbinder.content = unbinder.content.replace(/\|/g, "{{subst:!}}");
return unbinder.rebind();
},
// a replacement for String.prototype.replace() when the second parameter (the
// replacement string) is arbitrary, such as a username or freeform user input,
// and may contain dollar signs
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) {
return string.replace(pattern, replacement.replace(/\$/g, "$$$$"));
}
}
};
};
Line 989: Line 1,063:
Morebits.array = {
Morebits.array = {
uniq: function(arr) {
uniq: function(arr) {
if ( ! $.isArray( arr ) ) {
if ( ! Array.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.uniq";
throw "A non-array object passed to Morebits.array.uniq";
}
}
Line 1,002: Line 1,076:
},
},
dups: function(arr) {
dups: function(arr) {
if ( ! $.isArray( arr ) ) {
if ( ! Array.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.dups";
throw "A non-array object passed to Morebits.array.dups";
}
}
Line 1,018: Line 1,092:
},
},
chunk: function( arr, size ) {
chunk: function( arr, size ) {
if ( ! $.isArray( arr ) ) {
if ( ! Array.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.chunk";
throw "A non-array object passed to Morebits.array.chunk";
}
}
Line 1,035: Line 1,109:
return result;
return result;
}
}
};
/**
* **************** Morebits.getPageAssociatedUser ****************
* Get the user associated with the currently-viewed page.
* Currently works on User:, User talk:, Special:Contributions.
*/
Morebits.getPageAssociatedUser = function(){
var thisNamespaceId = mw.config.get('wgNamespaceNumber');
if ( thisNamespaceId === 2 /* User: */ || thisNamespaceId === 3 /* User talk: */ ) {
return mw.config.get('wgTitle').split( '/' )[0];  // only first part before any slashes, to work on subpages
}
if ( thisNamespaceId === -1 /* Special: */ && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) {
return $('table.mw-contributions-table input[name="target"]')[0].getAttribute('value');
}
return false;
};
};


Line 1,109: Line 1,161:


Morebits.unbinder.getCallback = function UnbinderGetCallback(self) {
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback( match , a , b ) {
return function UnbinderCallback( match ) {
var current = self.prefix + self.counter + self.postfix;
var current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
self.history[current] = match;
Line 1,205: Line 1,257:
'108': 'Book',
'108': 'Book',
'109': 'Book talk',
'109': 'Book talk',
'118': 'Draft',
'119': 'Draft talk',
'446': 'Education Program',
'446': 'Education Program',
'447': 'Education Program talk',
'447': 'Education Program talk',
Line 1,234: Line 1,288:
'108': 'Book',
'108': 'Book',
'109': 'Book talk',
'109': 'Book talk',
'118': 'Draft',
'119': 'Draft talk',
'446': 'Education Program',
'446': 'Education Program',
'447': 'Education Program talk',
'447': 'Education Program talk',
Line 1,265: Line 1,321:
  *    Every call to Morebits.wiki.api.post() results in the dispatch of
  *    Every call to Morebits.wiki.api.post() results in the dispatch of
  *    an asynchronous callback. Each callback can in turn
  *    an asynchronous callback. Each callback can in turn
  *    make an additional call to Morebits.wiki.api.post() to continue a  
  *    make an additional call to Morebits.wiki.api.post() to continue a
  *    processing sequence. At the conclusion of the final callback
  *    processing sequence. At the conclusion of the final callback
  *    of a processing sequence, it is not possible to simply return to the
  *    of a processing sequence, it is not possible to simply return to the
Line 1,276: Line 1,332:
  *    is managed through the globals Morebits.wiki.numberOfActionsLeft and
  *    is managed through the globals Morebits.wiki.numberOfActionsLeft and
  *    Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
  *    Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
  *    incremented at the start of every Morebits.wiki.api call and decremented  
  *    incremented at the start of every Morebits.wiki.api call and decremented
  *    after the completion of a callback function. If a callback function
  *    after the completion of a callback function. If a callback function
  *    does not create a new Morebits.wiki.api object before exiting, it is the
  *    does not create a new Morebits.wiki.api object before exiting, it is the
Line 1,285: Line 1,341:
  *    processing is not complete upon the conclusion of the final callback function.
  *    processing is not complete upon the conclusion of the final callback function.
  *    This is used for batch operations. The end of a batch is signaled by calling
  *    This is used for batch operations. The end of a batch is signaled by calling
  *    Morebits.wiki.removeCheckpoint().  
  *    Morebits.wiki.removeCheckpoint().
  */
  */


Line 1,302: Line 1,358:
if( Morebits.wiki.actionCompleted.redirect ) {
if( Morebits.wiki.actionCompleted.redirect ) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if( !( (/^\w+\:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) {
if( !( (/^\w+:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) {
Morebits.wiki.actionCompleted.redirect = mw.util.wikiGetlink( Morebits.wiki.actionCompleted.redirect );
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl( Morebits.wiki.actionCompleted.redirect );
if( Morebits.wiki.actionCompleted.followRedirect === false ) {
if( Morebits.wiki.actionCompleted.followRedirect === false ) {
Morebits.wiki.actionCompleted.redirect += "?redirect=no";
Morebits.wiki.actionCompleted.redirect += "?redirect=no";
Line 1,342: Line 1,398:
this.query = query;
this.query = query;
this.query.format = 'xml';
this.query.format = 'xml';
this.query.assert = 'user';
this.onSuccess = onSuccess;
this.onSuccess = onSuccess;
this.onError = onError;
this.onError = onError;
Line 1,376: Line 1,433:
url: mw.util.wikiScript('api'),
url: mw.util.wikiScript('api'),
data: Morebits.queryString.create(this.query),
data: Morebits.queryString.create(this.query),
datatype: 'xml'
dataType: 'xml',
headers: {
'Api-User-Agent': morebitsWikiApiUserAgent
}
}, callerAjaxParameters );
}, callerAjaxParameters );


return $.ajax( ajaxparams ).done(
return $.ajax( ajaxparams ).done(
function(xml, statusText, jqXHR) {
function(xml, statusText) {
this.statusText = statusText;
this.statusText = statusText;
this.responseXML = xml;
this.responseXML = xml;
Line 1,417: Line 1,477:


returnError: function() {
returnError: function() {
this.statelem.error( this.errorText );
if ( this.errorCode === "badtoken" ) {
this.statelem.error( "Invalid token. Refresh the page and try again" );
} else {
this.statelem.error( this.errorText );
}


// invoke failure callback if one was supplied
// invoke failure callback if one was supplied
Line 1,444: Line 1,508:
return this.responseXML;
return this.responseXML;
}
}
};
// Custom user agent header, used by WMF for server-side logging
// See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html
var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])';
// Sets the custom user agent header
Morebits.wiki.api.setApiUserAgent = function( ua ) {
morebitsWikiApiUserAgent = ( ua ? ua + ' ' : '' ) + 'morebits.js/2.0 ([[w:WT:TW]])';
};
};


Line 1,473: Line 1,546:
  *    onSuccess - callback function which is called when the load has succeeded
  *    onSuccess - callback function which is called when the load has succeeded
  *    onFailure - callback function which is called when the load fails (optional)
  *    onFailure - callback function which is called when the load fails (optional)
*                XXX onFailure for load() is not yet implemented – do we need it? -- UncleDouggie
*                    probably not -- TTO
  *
  *
  * save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load().
  * save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load().
Line 1,480: Line 1,551:
  *    onFailure - callback function which is called when the save fails (optional)
  *    onFailure - callback function which is called when the save fails (optional)
  *    Warning: Calling save() can result in additional calls to the previous load() callbacks to
  *    Warning: Calling save() can result in additional calls to the previous load() callbacks to
  *            recover from edit conflicts!  
  *            recover from edit conflicts!
  *            In this case, callers must make the same edit to the new pageText and reinvoke save().  
  *            In this case, callers must make the same edit to the new pageText and reinvoke save().
  *            This behavior can be disabled with setMaxConflictRetries(0).
  *            This behavior can be disabled with setMaxConflictRetries(0).
  *
  *
Line 1,498: Line 1,569:
  * getPageText(): returns a string containing the text of the page after a successful load()
  * getPageText(): returns a string containing the text of the page after a successful load()
  *
  *
  * setPageText(pageText)  
  * setPageText(pageText)
  *    pageText - string containing the updated page text that will be saved when save() is called
  *    pageText - string containing the updated page text that will be saved when save() is called
  *
  *
  * setAppendText(appendText)  
  * setAppendText(appendText)
  *    appendText - string containing the text that will be appended to the page when append() is called
  *    appendText - string containing the text that will be appended to the page when append() is called
  *
  *
  * setPrependText(prependText)  
  * setPrependText(prependText)
  *    prependText - string containing the text that will be prepended to the page when prepend() is called
  *    prependText - string containing the text that will be prepended to the page when prepend() is called
  *
  *
Line 1,510: Line 1,581:
  *    summary - string containing the text of the edit summary that will be used when save() is called
  *    summary - string containing the text of the edit summary that will be used when save() is called
  *
  *
  * setMinorEdit(minorEdit)  
  * setMinorEdit(minorEdit)
  *    minorEdit is a boolean value:
  *    minorEdit is a boolean value:
  *      true  - When save is called, the resulting edit will be marked as "minor".
  *      true  - When save is called, the resulting edit will be marked as "minor".
  *      false - When save is called, the resulting edit will not be marked as "minor". (default)
  *      false - When save is called, the resulting edit will not be marked as "minor". (default)
  *
  *
  * setBotEdit(botEdit)  
  * setBotEdit(botEdit)
  *    botEdit is a boolean value:
  *    botEdit is a boolean value:
  *      true  - When save is called, the resulting edit will be marked as "bot".
  *      true  - When save is called, the resulting edit will be marked as "bot".
Line 1,548: Line 1,619:
  *    followRedirect is a boolean value:
  *    followRedirect is a boolean value:
  *      true  - a maximum of one redirect will be followed.
  *      true  - a maximum of one redirect will be followed.
  *              In the event of a redirect, a message is displayed to the user and  
  *              In the event of a redirect, a message is displayed to the user and
  *              the redirect target can be retrieved with getPageName().
  *              the redirect target can be retrieved with getPageName().
  *      false - the requested pageName will be used without regard to any redirect. (default)
  *      false - the requested pageName will be used without regard to any redirect. (default)
Line 1,559: Line 1,630:
  * setWatchlistFromPreferences(watchlistOption)
  * setWatchlistFromPreferences(watchlistOption)
  *    watchlistOption is a boolean value:
  *    watchlistOption is a boolean value:
  *      true  - page watchlist status will be set based on the user's  
  *      true  - page watchlist status will be set based on the user's
  *              preference settings when save() is called
  *              preference settings when save() is called
  *      false - watchlist status of the page will not be changed (default)
  *      false - watchlist status of the page will not be changed (default)
Line 1,587: Line 1,658:
  *    onSuccess - callback function which is called when the username is found
  *    onSuccess - callback function which is called when the username is found
  *                within the callback, the username can be retrieved using the getCreator() function
  *                within the callback, the username can be retrieved using the getCreator() function
  *  
  *
  * getCreator(): returns the user who created the page following lookupCreator()
  * getCreator(): returns the user who created the page following lookupCreator()
  *
  *
Line 1,604: Line 1,675:
  *
  *
  *    Edit current contents of a page (no edit conflict):
  *    Edit current contents of a page (no edit conflict):
  *      .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->  
  *      .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->  
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *
  *
  *    Edit current contents of a page (with edit conflict):
  *    Edit current contents of a page (with edit conflict):
  *      .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->  
  *      .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->  
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() ->
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() ->
  *            ctx.loadApi.post() -> ctx.loadApi.post.success() ->  
  *            ctx.loadApi.post() -> ctx.loadApi.post.success() ->
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->  
  *            ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *
  *
  *    Append to a page (similar for prepend):
  *    Append to a page (similar for prepend):
  *      .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->  
  *      .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
  *            ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() ->  
  *            ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() ->
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *            ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
  *
  *
  *    Notes:  
  *    Notes:
  *      1. All functions following Morebits.wiki.api.post() are invoked asynchronously  
  *      1. All functions following Morebits.wiki.api.post() are invoked asynchronously
  *          from the jQuery AJAX library.
  *          from the jQuery AJAX library.
  *      2. The sequence for append/prepend could be slightly shortened, but it would require
  *      2. The sequence for append/prepend could be slightly shortened, but it would require
Line 1,641: Line 1,712:
*/
*/
var ctx = {
var ctx = {
// backing fields for public properties
// backing fields for public properties
pageName: pageName,
pageName: pageName,
pageExists: false,
pageExists: false,
Line 1,647: Line 1,718:
callbackParameters: null,
callbackParameters: null,
statusElement: new Morebits.status(currentAction),
statusElement: new Morebits.status(currentAction),
// - edit
 
// - edit
pageText: null,
pageText: null,
editMode: 'all',  // save() replaces entire contents of the page by default
editMode: 'all',  // save() replaces entire contents of the page by default
Line 1,661: Line 1,733:
watchlistOption: 'nochange',
watchlistOption: 'nochange',
creator: null,
creator: null,
// - revert
 
// - revert
revertOldID: null,
revertOldID: null,
// - move
 
// - move
moveDestination: null,
moveDestination: null,
moveTalkPage: false,
moveTalkPage: false,
moveSubpages: false,
moveSubpages: false,
moveSuppressRedirect: false,
moveSuppressRedirect: false,
// - protect
 
// - protect
protectEdit: null,
protectEdit: null,
protectMove: null,
protectMove: null,
protectCreate: null,
protectCreate: null,
protectCascade: false,
protectCascade: false,
// - stabilize (FlaggedRevs)
 
// - stabilize (FlaggedRevs)
flaggedRevs: null,
flaggedRevs: null,
// internal status
 
// internal status
pageLoaded: false,
pageLoaded: false,
editToken: null,
editToken: null,
Line 1,686: Line 1,763:
conflictRetries: 0,
conflictRetries: 0,
retries: 0,
retries: 0,
// callbacks
 
// callbacks
onLoadSuccess: null,
onLoadSuccess: null,
onLoadFailure: null,
onLoadFailure: null,
Line 1,700: Line 1,778:
onStabilizeSuccess: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
onStabilizeFailure: null,
// internal objects
 
// internal objects
loadQuery: null,
loadQuery: null,
loadApi: null,
loadApi: null,
Line 1,887: Line 1,966:


if (ctx.editMode === 'all') {
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content';  // get the page content at the same time, if needed
ctx.loadQuery.rvprop = 'content|timestamp';  // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvprop = 'timestamp';
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
ctx.loadQuery.rvstartid = ctx.revertOldID;
Line 1,914: Line 1,994:
ctx.onSaveFailure = onFailure || emptyFunction;
ctx.onSaveFailure = onFailure || emptyFunction;


if (!ctx.pageLoaded) {
// are we getting our edit token from mw.user.tokens?
var canUseMwUserToken = fnCanUseMwUserToken('edit');
 
if (!ctx.pageLoaded && !canUseMwUserToken) {
ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!");
ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!");
ctx.onSaveFailure(this);
ctx.onSaveFailure(this);
Line 1,925: Line 2,008:
}
}


if (ctx.fullyProtected && !ctx.suppressProtectWarning &&  
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm('You are about to make an edit to the fully protected page "' + ctx.pageName +
!confirm('You are about to make an edit to the fully protected page "' + ctx.pageName +
(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) +
(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) +
Line 1,940: Line 2,024:
title: ctx.pageName,
title: ctx.pageName,
summary: ctx.editSummary,
summary: ctx.editSummary,
token: ctx.editToken,
token: canUseMwUserToken ? mw.user.tokens.get('editToken') : ctx.editToken,
watchlist: ctx.watchlistOption
watchlist: ctx.watchlistOption
};
};
Line 1,986: Line 2,070:
if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) {
if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) {
query[ctx.createOption] = '';
query[ctx.createOption] = '';
}
if (canUseMwUserToken && ctx.followRedirect) {
query.redirect = true;
}
}


Line 1,995: Line 2,083:
this.append = function(onSuccess, onFailure) {
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
ctx.editMode = 'append';
ctx.onSaveSuccess = onSuccess;
 
ctx.onSaveFailure = onFailure || emptyFunction;
if (fnCanUseMwUserToken('edit')) {
this.load(fnAutoSave, ctx.onSaveFailure);
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
};


this.prepend = function(onSuccess, onFailure) {
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
ctx.editMode = 'prepend';
ctx.onSaveSuccess = onSuccess;
 
ctx.onSaveFailure = onFailure || emptyFunction;
if (fnCanUseMwUserToken('edit')) {
this.load(fnAutoSave, ctx.onSaveFailure);
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
};


Line 2,121: Line 2,219:
}
}


var query = {
if (fnCanUseMwUserToken('delete')) {
action: 'query',
fnProcessDelete.call(this, this);
prop: 'info',
} else {
inprop: 'protection',
var query = {
intoken: 'delete',
action: 'query',
titles: ctx.pageName
prop: 'info',
};
inprop: 'protection',
if (ctx.followRedirect) {
intoken: 'delete',
query.redirects = '';  // follow all redirects
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = '';  // follow all redirects
}
 
ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
}
}
ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
};
};


Line 2,158: Line 2,260:
}
}


// because of the way MW API interprets protection levels (absolute, not
// differential), we need to request protection levels from the server
var query = {
var query = {
action: 'query',
action: 'query',
Line 2,163: Line 2,267:
inprop: 'protection',
inprop: 'protection',
intoken: 'protect',
intoken: 'protect',
titles: ctx.pageName
titles: ctx.pageName,
watchlist: ctx.watchlistOption
};
};
if (ctx.followRedirect) {
if (ctx.followRedirect) {
Line 2,212: Line 2,317:
ctx.stabilizeApi.post();
ctx.stabilizeApi.post();
};
};
/* Private member functions
*
* These are not exposed outside
*/


/**
/**
* Private member functions
* Determines whether we can save an API call by using the edit token sent with the page
* HTML, or whether we need to ask the server for more info (e.g. protection expiry).
*
* Currently only used for append, prepend, and deletePage.
*
*
* These are not exposed outside
* @param {string} action  The action being undertaken, e.g. "edit", "delete".
*/
*/
var fnCanUseMwUserToken = function(action) {
// API-based redirect resolution only works for action=query and
// action=edit in append/prepend modes (and section=new, but we don't
// really support that)
if (ctx.followRedirect && (action !== 'edit' ||
(ctx.editMode !== 'append' && ctx.editMode !== 'prepend'))) {
return false;
}
// do we need to fetch the edit protection expiry?
if (Morebits.userIsInGroup('sysop') && !ctx.suppressProtectWarning) {
// poor man's normalisation
if (Morebits.string.toUpperCaseFirstChar(mw.config.get('wgPageName')).replace(/ /g, '_').trim() !==
Morebits.string.toUpperCaseFirstChar(ctx.pageName).replace(/ /g, '_').trim()) {
return false;
}
var editRestriction = mw.config.get('wgRestrictionEdit');
if (!editRestriction || editRestriction.indexOf('sysop') !== -1) {
return false;
}
}
return !!mw.user.tokens.get('editToken');
};


// callback from loadSuccess() for append() and prepend() threads
// callback from loadSuccess() for append() and prepend() threads
Line 2,261: Line 2,399:
return;
return;
}
}
ctx.lastEditTime = $(xml).find('page').attr('touched');
ctx.lastEditTime = $(xml).find('rev').attr('timestamp');
ctx.revertCurID = $(xml).find('page').attr('lastrevid');
ctx.revertCurID = $(xml).find('page').attr('lastrevid');


Line 2,337: Line 2,475:
// default on success action - display link for edited page
// default on success action - display link for edited page
var link = document.createElement('a');
var link = document.createElement('a');
link.setAttribute('href', mw.util.wikiGetlink(ctx.pageName) );
link.setAttribute('href', mw.util.getUrl(ctx.pageName) );
link.appendChild(document.createTextNode(ctx.pageName));
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['completed (', link, ')']);
ctx.statusElement.info(['completed (', link, ')']);
Line 2,394: Line 2,532:


var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement);
var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement);
var result = purgeApi.post( { async: false } );  // just wait for it, result is for debugging
purgeApi.post( { async: false } );  // just wait for it, result is for debugging


--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds


ctx.statusElement.info("Edit conflict detected, reapplying edit");
ctx.statusElement.info("Edit conflict detected, reapplying edit");
ctx.loadApi.post(); // reload the page and reapply the edit
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append or prepend, so this should work as desired
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}


// check for loss of edit token
// check for loss of edit token
Line 2,407: Line 2,549:
ctx.statusElement.info("Edit token is invalid, retrying");
ctx.statusElement.info("Edit token is invalid, retrying");
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds
ctx.loadApi.post(); // reload
if (fnCanUseMwUserToken('edit')) {
this.load(fnAutoSave, ctx.onSaveFailure); // try the append or prepend again
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}


// check for network or server error
// check for network or server error
Line 2,460: Line 2,606:
if (Morebits.userIsInGroup('sysop')) {
if (Morebits.userIsInGroup('sysop')) {
var editprot = $(xml).find('pr[type="edit"]');
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&  
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
!confirm('You are about to move the fully protected page "' + ctx.pageName +
!confirm('You are about to move the fully protected page "' + ctx.pageName +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
Line 2,503: Line 2,649:


var fnProcessDelete = function() {
var fnProcessDelete = function() {
var xml = ctx.deleteApi.getXML();
var pageTitle, token;


if ($(xml).find('page').attr('missing') === "") {
if (fnCanUseMwUserToken('delete')) {
ctx.statusElement.error("Cannot delete the page, because it no longer exists");
token = mw.user.tokens.get('editToken');
ctx.onDeleteFailure(this);
pageTitle = ctx.pageName;
return;
} else {
}
var xml = ctx.deleteApi.getXML();


// extract protection info
if ($(xml).find('page').attr('missing') === "") {
var editprot = $(xml).find('pr[type="edit"]');
ctx.statusElement.error("Cannot delete the page, because it no longer exists");
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
ctx.onDeleteFailure(this);
!confirm('You are about to delete the fully protected page "' + ctx.pageName +
return;
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
}
'.  \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) {
ctx.statusElement.error("Deletion of fully protected page was aborted.");
ctx.onDeleteFailure(this);
return;
}


var deleteToken = $(xml).find('page').attr('deletetoken');
// extract protection info
if (!deleteToken) {
var editprot = $(xml).find('pr[type="edit"]');
ctx.statusElement.error("Failed to retrieve delete token.");
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
ctx.onDeleteFailure(this);
!confirm('You are about to delete the fully protected page "' + ctx.pageName +
return;
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
}
'.  \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) {
ctx.statusElement.error("Deletion of fully protected page was aborted.");
ctx.onDeleteFailure(this);
return;
}
 
token = $(xml).find('page').attr('deletetoken');
if (!token) {
ctx.statusElement.error("Failed to retrieve delete token.");
ctx.onDeleteFailure(this);
return;
}
 
pageTitle = $(xml).find('page').attr('title');
}


var query = {
var query = {
'action': 'delete',
'action': 'delete',
'title': $(xml).find('page').attr('title'),
'title': pageTitle,
'token': deleteToken,
'token': token,
'reason': ctx.editSummary
'reason': ctx.editSummary
};
};
Line 2,539: Line 2,694:
}
}


ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
ctx.deleteProcessApi.post();
};
// callback from deleteProcessApi.post()
var fnProcessDeleteError = function() {
var errorCode = ctx.deleteProcessApi.getErrorCode();
// check for "Database query error"
if ( errorCode === "internal_api_error_DBQueryError" && ctx.retries++ < ctx.maxRetries ) {
ctx.statusElement.info("Database query error, retrying");
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds
ctx.deleteProcessApi.post(); // give it another go!
} else if ( errorCode === "badtoken" ) {
// this is pathetic, but given the current state of Morebits.wiki.page it would
// be a dog's breakfast to try and fix this
ctx.statusElement.error("Invalid token. Please refresh the page and try again.");
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, this, ctx.deleteProcessApi);
}
} else if ( errorCode === "missingtitle" ) {
ctx.statusElement.error("Cannot delete the page, because it no longer exists");
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi);  // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error( "Failed to delete the page: " + ctx.deleteProcessApi.getErrorText() );
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi);  // invoke callback
}
}
};
};


Line 2,675: Line 2,867:
  *                        to render the specified wikitext.
  *                        to render the specified wikitext.
  *    wikitext - wikitext to render; most things should work, including subst: and ~~~~
  *    wikitext - wikitext to render; most things should work, including subst: and ~~~~
*    pageTitle - optional parameter for the page this should be rendered as being on
  *
  *
  * closePreview(): Hides the preview box and clears it.
  * closePreview(): Hides the preview box and clears it.
Line 2,688: Line 2,881:
$(previewbox).addClass("morebits-previewbox").hide();
$(previewbox).addClass("morebits-previewbox").hide();


this.beginRender = function(wikitext) {
this.beginRender = function(wikitext, pageTitle) {
$(previewbox).show();
$(previewbox).show();


Line 2,700: Line 2,893:
pst: 'true',  // PST = pre-save transform; this makes substitution work properly
pst: 'true',  // PST = pre-save transform; this makes substitution work properly
text: wikitext,
text: wikitext,
title: mw.config.get('wgPageName')
title: pageTitle || mw.config.get('wgPageName')
};
};
var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview"));
var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview"));
Line 2,714: Line 2,907:
}
}
previewbox.innerHTML = html;
previewbox.innerHTML = html;
$(previewbox).find("a").attr("target", "_blank");
};
};


Line 2,763: Line 2,957:
continue;
continue;
}
}
if( test2 === '[[' ) {
if( test2 === ']]' ) {
current += test2;
current += test2;
++i;
++i;
Line 2,831: Line 3,025:
var first_char = link_target.substr( 0, 1 );
var first_char = link_target.substr( 0, 1 );
var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true );
var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true );
var link_simple_re = new RegExp( "\\[\\[:?(" + link_re_string + ")\\]\\]", 'g' );
 
var link_named_re = new RegExp( "\\[\\[:?" + link_re_string + "\\|(.+?)\\]\\]", 'g' );
// Files and Categories become links with a leading colon.
// e.g. [[:File:Test.png]]
var special_ns_re = /^(?:[Ff]ile|[Ii]mage|[Cc]ategory):/;
var colon = special_ns_re.test( link_target ) ? ':' : '';
 
var link_simple_re = new RegExp( "\\[\\[" + colon + "(" + link_re_string + ")\\]\\]", 'g' );
var link_named_re = new RegExp( "\\[\\[" + colon + link_re_string + "\\|(.+?)\\]\\]", 'g' );
this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" );
this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" );
},
},
Line 2,931: Line 3,131:
  *    returns the query string as a string
  *    returns the query string as a string
  * Morebits.queryString.create( hash )
  * Morebits.queryString.create( hash )
  *    creates an querystring and encodes strings via encodeURIComponent and joins arrays with |  
  *    creates an querystring and encodes strings via encodeURIComponent and joins arrays with |
  *
  *
  * In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object.
  * In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object.
Line 3,016: Line 3,216:
}
}
var res;
var res;
if( $.isArray( arr[i] ) ){
if( Array.isArray( arr[i] ) ){
var v = [];
var v = [];
for(var j = 0; j < arr[i].length; ++j ) {
for(var j = 0; j < arr[i].length; ++j ) {
Line 3,068: Line 3,268:


Morebits.status.onError = function( handler ) {
Morebits.status.onError = function( handler ) {
if ( $.isFunction( handler ) ) {
if ( typeof handler === 'function' ) {
Morebits.status.errorEvent = handler;
Morebits.status.errorEvent = handler;
} else {
} else {
Line 3,096: Line 3,296:
},
},
codify: function( obj ) {
codify: function( obj ) {
if ( ! $.isArray( obj ) ) {
if ( ! Array.isArray( obj ) ) {
obj = [ obj ];
obj = [ obj ];
}
}
Line 3,118: Line 3,318:
// hack to force the page not to reload when an error is output - see also Morebits.status() above
// hack to force the page not to reload when an error is output - see also Morebits.status() above
Morebits.wiki.numberOfActionsLeft = 1000;
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
// call error callback
if (Morebits.status.errorEvent) {
if (Morebits.status.errorEvent) {
Morebits.status.errorEvent();
Morebits.status.errorEvent();
}
}
// also log error messages in the browser console
// also log error messages in the browser console
if (console && console.error) {
console.error(this.textRaw + ": " + status); // eslint-disable-line no-console
console.error(this.textRaw + ": " + status);
}
}
}
}
}
Line 3,169: Line 3,369:
Morebits.status.error = function( text, status ) {
Morebits.status.error = function( text, status ) {
return new Morebits.status( text, status, 'error' );
return new Morebits.status( text, status, 'error' );
};
// display the user's rationale, comments, etc. back to them after a failure,
// so they don't use it
Morebits.status.printUserText = function( comments, message ) {
var p = document.createElement( 'p' );
p.textContent = message;
var div = document.createElement( 'div' );
div.className = 'toccolours';
div.style.marginTop = '0';
div.style.whiteSpace = 'pre-wrap';
div.textContent = comments;
p.appendChild( div );
Morebits.status.root.appendChild( p );
};
};


Line 3,190: Line 3,404:


/**
/**
  * **************** Morebits.simpleWindow ****************
  * **************** Morebits.checkboxClickHandler() ****************
  * A simple draggable window
  * shift-click-support for checkboxes
  * now a wrapper for jQuery UI's dialog feature
* wikibits version (window.addCheckboxClickHandlers) has some restrictions, and
  * doesn't work with checkboxes inside a sortable table, so let's build our own.
  */
  */


// The height passed in here is the maximum allowable height for the content area.
Morebits.checkboxShiftClickSupport = function (jQuerySelector, jQueryContext) {
Morebits.simpleWindow = function SimpleWindow( width, height ) {
var lastCheckbox = null;
var content = document.createElement( 'div' );
this.content = content;
content.className = 'morebits-dialog-content';


this.height = height;
function clickHandler(event) {
var thisCb = this;
if (event.shiftKey && lastCheckbox !== null) {
var cbs = $(jQuerySelector, jQueryContext); //can't cache them, obviously, if we want to support resorting
var index = -1, lastIndex = -1, i;
for (i = 0; i < cbs.length; i++) {
if (cbs[i] === thisCb) {
index = i;
if (lastIndex > -1)
break;
}
if (cbs[i] === lastCheckbox) {
lastIndex = i;
if (index > -1)
break;
}
}
 
if (index > -1 && lastIndex > -1) {
//inspired by wikibits
var endState = thisCb.checked;
var start, finish;
if (index < lastIndex) {
start = index + 1;
finish = lastIndex;
} else {
start = lastIndex;
finish = index - 1;
}


$(this.content).dialog({
for (i = start; i <= finish; i++) {
autoOpen: false,
cbs[i].checked = endState;
buttons: { "Placeholder button": function() {} },
}
dialogClass: 'morebits-dialog',
width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)),
// give jQuery the given height value (which represents the anticipated height of the dialog) here, so
// it can position the dialog appropriately
// the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared
// to that of the old SimpleWindow
height: height + 20,
close: function(event, ui) {
// dialogs and their content can be destroyed once closed
$(event.target).dialog("destroy").remove();
},
resize: function(event, ui) {
this.style.maxHeight = "";
}
}
});
}
lastCheckbox = thisCb;
return true;
}


var $widget = $(this.content).dialog("widget");
  $(jQuerySelector, jQueryContext).click(clickHandler);
};


// add background gradient to titlebar
var $titlebar = $widget.find(".ui-dialog-titlebar");
var oldstyle = $titlebar.attr("style");
$titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;');


// delete the placeholder button (it's only there so the buttonpane gets created)
$widget.find("button").each(function(key, value) {
value.parentNode.removeChild(value);
});


// add container for the buttons we add, and the footer links (if any)
/** **************** Morebits.batchOperation ****************
var buttonspan = document.createElement("span");
* Iterates over a group of pages and executes a worker function for each.
buttonspan.className = "morebits-dialog-buttons";
*
var linksspan = document.createElement("span");
* Constructor: Morebits.batchOperation(currentAction)
linksspan.className = "morebits-dialog-footerlinks";
*
$widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan);
* setPageList(wikitext): Sets the list of pages to work on.
};
*    It should be an array of page names (strings).
 
*
Morebits.simpleWindow.prototype = {
* setOption(optionName, optionValue): Sets a known option:
buttons: [],
*    - chunkSize (integer): the size of chunks to break the array into (default 50).
height: 600,
*                          Setting this to a small value (<5) can cause problems.
hasFooterLinks: false,
*    - preserveIndividualStatusLines (boolean): keep each page's status element visible
scriptName: null,
*                                              when worker is complete?  See note below
 
*
// Focuses the dialog. This might work, or on the contrary, it might not.
* run(worker): Runs the given callback for each page in the list.
focus: function(event) {
*    The callback must call workerSuccess when succeeding, or workerFailure
$(this.content).dialog("moveToTop");
*    when failing.  If using Morebits.wiki.api or Morebits.wiki.page, this is easily
 
*    done by passing these two functions as parameters to the methods on those
return this;
*    objects, for instance, page.save(batchOp.workerSuccess, batchOp.workerFailure).
},
*    Make sure the methods are called directly if special success/failure cases arise.
// Closes the dialog.  If this is set as an event handler, it will stop the event from doing anything more.
*    If you omit to call these methods, the batch operation will stall after the first
close: function(event) {
*    chunk!  Also ensure that either workerSuccess or workerFailure is called no more
if (event) {
*    than once.
event.preventDefault();
*
}
* If using preserveIndividualStatusLines, you should try to ensure that the
$(this.content).dialog("close");
* workerSuccess callback has access to the page title.  This is no problem for
 
* Morebits.wiki.page objects.  But when using the API, please set the
return this;
* |pageName| property on the Morebits.wiki.api object.
},
*
// Shows the dialog.  Calling display() on a dialog that has previously been closed might work, but it is not guaranteed.
* There are sample batchOperation implementations using Morebits.wiki.page in
display: function() {
* twinklebatchdelete.js, and using Morebits.wiki.api in twinklebatchundelete.js.
if (this.scriptName) {
*/
var $widget = $(this.content).dialog("widget");
 
$widget.find(".morebits-dialog-scriptname").remove();
Morebits.batchOperation = function(currentAction) {
var scriptnamespan = document.createElement("span");
var ctx = {
scriptnamespan.className = "morebits-dialog-scriptname";
// backing fields for public properties
scriptnamespan.textContent = this.scriptName + " \u00B7 ";  // U+00B7 MIDDLE DOT = &middot;
pageList: null,
$widget.find(".ui-dialog-title").prepend(scriptnamespan);
options: {
}
chunkSize: 50,
preserveIndividualStatusLines: false
},
 
// internal counters, etc.
statusElement: new Morebits.status(currentAction || "Performing batch operation"),
worker: null,
countStarted: 0,
countFinished: 0,
countFinishedSuccess: 0,
currentChunkIndex: -1,
pageChunks: [],
running: false
};
 
// shouldn't be needed by external users, but provided anyway for maximum flexibility
this.getStatusElement = function() {
return ctx.statusElement;
};
 
this.setPageList = function(pageList) {
ctx.pageList = pageList;
};
 
this.setOption = function(optionName, optionValue) {
ctx.options[optionName] = optionValue;
};
 
this.run = function(worker) {
if (ctx.running) {
ctx.statusElement.error("Batch operation is already running");
return;
}
ctx.running = true;
 
ctx.worker = worker;
ctx.countStarted = 0;
ctx.countFinished = 0;
ctx.countFinishedSuccess = 0;
ctx.currentChunkIndex = -1;
ctx.pageChunks = [];
 
var total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info("nothing to do");
ctx.running = false;
return;
}
 
// chunk page list into more manageable units
ctx.pageChunks = Morebits.array.chunk(ctx.pageList, ctx.options.chunkSize);
 
// start the process
Morebits.wiki.addCheckpoint();
ctx.statusElement.status("0%");
fnStartNewChunk();
};
 
this.workerSuccess = function(apiobj) {
// update or remove status line
if (apiobj && apiobj.getStatusElement) {
var statelem = apiobj.getStatusElement();
if (ctx.options.preserveIndividualStatusLines) {
if (apiobj.getPageName || apiobj.pageName || (apiobj.query && apiobj.query.title)) {
// we know the page title - display a relevant message
var pageName = apiobj.getPageName ? apiobj.getPageName() :
(apiobj.pageName || apiobj.query.title);
var link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(pageName));
link.appendChild(document.createTextNode(pageName));
statelem.info(['completed (', link, ')']);
} else {
// we don't know the page title - just display a generic message
statelem.info('done');
}
} else {
// remove the status line from display
statelem.unlink();
}
}
 
ctx.countFinishedSuccess++;
fnDoneOne(apiobj);
};
 
this.workerFailure = function(apiobj) {
fnDoneOne(apiobj);
};
 
// private functions
 
var thisProxy = this;
 
var fnStartNewChunk = function() {
var chunk = ctx.pageChunks[++ctx.currentChunkIndex];
if (!chunk) {
return;  // done! yay
}
 
// start workers for the current chunk
ctx.countStarted += chunk.length;
chunk.forEach(function(page) {
ctx.worker(page, thisProxy);
});
};
 
var fnDoneOne = function() {
ctx.countFinished++;
 
// update overall status line
var total = ctx.pageList.length;
if (ctx.countFinished === total) {
var statusString = "Done (" + ctx.countFinishedSuccess +
"/" + ctx.countFinished + " actions completed successfully)";
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
} else {
ctx.statusElement.info(statusString);
}
Morebits.wiki.removeCheckpoint();
ctx.running = false;
return;
}
 
// just for giggles! (well, serious debugging, actually)
if (ctx.countFinished > total) {
ctx.statusElement.warn("Done (overshot by " + (ctx.countFinished - total) + ")");
Morebits.wiki.removeCheckpoint();
ctx.running = false;
return;
}
 
ctx.statusElement.status(parseInt(100 * ctx.countFinished / total, 10) + "%");
 
// start a new chunk if we're close enough to the end of the previous chunk, and
// we haven't already started the next one
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) &&
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
fnStartNewChunk();
}
};
};
 
 
 
/**
* **************** Morebits.simpleWindow ****************
* A simple draggable window
* now a wrapper for jQuery UI's dialog feature
*/
 
// The height passed in here is the maximum allowable height for the content area.
Morebits.simpleWindow = function SimpleWindow( width, height ) {
var content = document.createElement( 'div' );
this.content = content;
content.className = 'morebits-dialog-content';
content.id = 'morebits-dialog-content-' + Math.round(Math.random() * 1e15);
 
this.height = height;
 
$(this.content).dialog({
autoOpen: false,
buttons: { "Placeholder button": function() {} },
dialogClass: 'morebits-dialog',
width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)),
// give jQuery the given height value (which represents the anticipated height of the dialog) here, so
// it can position the dialog appropriately
// the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared
// to that of the old SimpleWindow
height: height + 20,
close: function(event) {
// dialogs and their content can be destroyed once closed
$(event.target).dialog("destroy").remove();
},
resizeStart: function() {
this.scrollbox = $(this).find(".morebits-scrollbox")[0];
if (this.scrollbox) {
this.scrollbox.style.maxHeight = "none";
}
},
resizeEnd: function() {
this.scrollbox = null;
},
resize: function() {
this.style.maxHeight = "";
if (this.scrollbox) {
this.scrollbox.style.width = "";
}
}
});
 
var $widget = $(this.content).dialog("widget");
 
// add background gradient to titlebar
var $titlebar = $widget.find(".ui-dialog-titlebar");
var oldstyle = $titlebar.attr("style");
$titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;');
 
// delete the placeholder button (it's only there so the buttonpane gets created)
$widget.find("button").each(function(key, value) {
value.parentNode.removeChild(value);
});
 
// add container for the buttons we add, and the footer links (if any)
var buttonspan = document.createElement("span");
buttonspan.className = "morebits-dialog-buttons";
var linksspan = document.createElement("span");
linksspan.className = "morebits-dialog-footerlinks";
$widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan);
 
// resize the scrollbox with the dialog, if one is present
$widget.resizable("option", "alsoResize", "#" + this.content.id + " .morebits-scrollbox, #" + this.content.id);
};
 
Morebits.simpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
 
// Focuses the dialog. This might work, or on the contrary, it might not.
focus: function() {
$(this.content).dialog("moveToTop");
 
return this;
},
// Closes the dialog.  If this is set as an event handler, it will stop the event from doing anything more.
close: function(event) {
if (event) {
event.preventDefault();
}
$(this.content).dialog("close");
 
return this;
},
// Shows the dialog.  Calling display() on a dialog that has previously been closed might work, but it is not guaranteed.
display: function() {
if (this.scriptName) {
var $widget = $(this.content).dialog("widget");
$widget.find(".morebits-dialog-scriptname").remove();
var scriptnamespan = document.createElement("span");
scriptnamespan.className = "morebits-dialog-scriptname";
scriptnamespan.textContent = this.scriptName + " \u00B7 ";  // U+00B7 MIDDLE DOT = &middot;
$widget.find(".ui-dialog-title").prepend(scriptnamespan);
}


var dialog = $(this.content).dialog("open");
var dialog = $(this.content).dialog("open");
if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) {  // tie in with NAVPOP
if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) {  // tie in with NAVPOP
dialog.parent()[0].ranSetupTooltipsAlready = false;
dialog.parent()[0].ranSetupTooltipsAlready = false;
setupTooltips(dialog.parent()[0]);
window.setupTooltips(dialog.parent()[0]);
}
}
this.setHeight( this.height );  // init height algorithm
this.setHeight( this.height );  // init height algorithm
Line 3,350: Line 3,817:
return this;
return this;
},
},
purgeContent: function( content ) {
purgeContent: function() {
this.buttons = [];
this.buttons = [];
// delete all buttons in the buttonpane
// delete all buttons in the buttonpane
Line 3,373: Line 3,840:
}
}
var link = document.createElement("a");
var link = document.createElement("a");
link.setAttribute("href", mw.util.wikiGetlink(wikiPage) );
link.setAttribute("href", mw.util.getUrl(wikiPage) );
link.setAttribute("title", wikiPage);
link.setAttribute("title", wikiPage);
link.setAttribute("target", "_blank");
link.setAttribute("target", "_blank");
Line 3,395: Line 3,862:
// Morebits.simpleWindow open, so this shouldn't matter.
// Morebits.simpleWindow open, so this shouldn't matter.
Morebits.simpleWindow.setButtonsEnabled = function( enabled ) {
Morebits.simpleWindow.setButtonsEnabled = function( enabled ) {
$(".morebits-dialog-buttons button").attr("disabled", !enabled);
$(".morebits-dialog-buttons button").prop("disabled", !enabled);
};
};


Line 3,416: Line 3,883:


if ( typeof arguments === "undefined" ) {  // typeof is here for a reason...
if ( typeof arguments === "undefined" ) {  // typeof is here for a reason...
/* global Morebits */
window.SimpleWindow = Morebits.simpleWindow;
window.SimpleWindow = Morebits.simpleWindow;
window.QuickForm = Morebits.quickForm;
window.QuickForm = Morebits.quickForm;
Line 3,423: Line 3,891:
}
}


 
//</nowiki>
// </nowiki>
14,061

edits