14,061
edits
(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 | return mw.config.get( 'wgUserGroups' ).indexOf( group ) !== -1; | ||
}; | }; | ||
Line 49: | Line 49: | ||
/** | /** | ||
* **************** Morebits. | * **************** Morebits.sanitizeIPv6() **************** | ||
* | * 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. | Morebits.sanitizeIPv6 = function ( address ) { | ||
return mw.util. | 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( ! | 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 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 ( ! | 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]; | ||
} | } | ||
}; | }; | ||
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 = | 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: | ||
} | } | ||
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( ! | 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 ) { | ||
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 ( ! | 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 ( ! | 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 ( ! | 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; | ||
} | } | ||
}; | }; | ||
Line 1,109: | Line 1,161: | ||
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { | Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { | ||
return function UnbinderCallback( match | 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+ | if( !( (/^\w+:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) { | ||
Morebits.wiki.actionCompleted.redirect = mw.util. | 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', | |||
headers: { | |||
'Api-User-Agent': morebitsWikiApiUserAgent | |||
} | |||
}, callerAjaxParameters ); | }, callerAjaxParameters ); | ||
return $.ajax( ajaxparams ).done( | return $.ajax( ajaxparams ).done( | ||
function(xml, statusText | 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) | ||
* | * | ||
* 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 | |||
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 | |||
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 | |||
revertOldID: null, | revertOldID: null, | ||
// - move | |||
moveDestination: null, | moveDestination: null, | ||
moveTalkPage: false, | moveTalkPage: false, | ||
moveSubpages: false, | moveSubpages: false, | ||
moveSuppressRedirect: false, | moveSuppressRedirect: false, | ||
// - protect | |||
protectEdit: null, | protectEdit: null, | ||
protectMove: null, | protectMove: null, | ||
protectCreate: null, | protectCreate: null, | ||
protectCascade: false, | protectCascade: false, | ||
// - stabilize (FlaggedRevs) | |||
flaggedRevs: null, | flaggedRevs: null, | ||
// 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 | |||
onLoadSuccess: null, | onLoadSuccess: null, | ||
onLoadFailure: null, | onLoadFailure: null, | ||
Line 1,700: | Line 1,778: | ||
onStabilizeSuccess: null, | onStabilizeSuccess: null, | ||
onStabilizeFailure: null, | onStabilizeFailure: null, | ||
// 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; | |||
if (fnCanUseMwUserToken('edit')) { | |||
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; | |||
if (fnCanUseMwUserToken('edit')) { | |||
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')) { | ||
fnProcessDelete.call(this, this); | |||
} else { | |||
var query = { | |||
action: 'query', | |||
prop: 'info', | |||
inprop: 'protection', | |||
intoken: 'delete', | |||
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(); | |||
} | } | ||
}; | }; | ||
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 | |||
*/ | |||
/** | /** | ||
* | * 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. | |||
* | * | ||
* | * @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(' | 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. | 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); | ||
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 | var pageTitle, token; | ||
if ( | if (fnCanUseMwUserToken('delete')) { | ||
ctx. | token = mw.user.tokens.get('editToken'); | ||
ctx. | pageTitle = ctx.pageName; | ||
} else { | |||
var xml = ctx.deleteApi.getXML(); | |||
if ($(xml).find('page').attr('missing') === "") { | |||
ctx.statusElement.error("Cannot delete the page, because it no longer exists"); | |||
ctx.onDeleteFailure(this); | |||
return; | |||
} | |||
// extract protection info | |||
var editprot = $(xml).find('pr[type="edit"]'); | |||
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning && | |||
!confirm('You are about to delete the fully protected page "' + ctx.pageName + | |||
(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': | 'title': pageTitle, | ||
'token': | '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.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( "\\[\\[ | |||
var link_named_re = new RegExp( "\\[\\[ | // 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( | 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 ( | 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 ( ! | 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 | ||
console.error(this.textRaw + ": " + status); // eslint-disable-line no-console | |||
} | } | ||
} | } | ||
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. | * **************** Morebits.checkboxClickHandler() **************** | ||
* | * shift-click-support for checkboxes | ||
* | * wikibits version (window.addCheckboxClickHandlers) has some restrictions, and | ||
* doesn't work with checkboxes inside a sortable table, so let's build our own. | |||
*/ | */ | ||
Morebits.checkboxShiftClickSupport = function (jQuerySelector, jQueryContext) { | |||
Morebits. | var lastCheckbox = null; | ||
var | |||
this. | 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; | |||
} | |||
for (i = start; i <= finish; i++) { | |||
cbs[i].checked = endState; | |||
} | |||
} | } | ||
} | } | ||
lastCheckbox = thisCb; | |||
return true; | |||
} | |||
$(jQuerySelector, jQueryContext).click(clickHandler); | |||
}; | |||
// 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( | * 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 = · | 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 = · | |||
$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( | 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. | 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"). | $(".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> |