Jump to content

MediaWiki:Gadget-morebits.js: Difference between revisions

robot: Import pages to supprt Twinkle
m (1 revision imported)
(robot: Import pages to supprt Twinkle)
 
Line 1: Line 1:
// <nowiki>
// <nowiki>
/**
/**
  * A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
  * A library full of lots of goodness for user scripts on MediaWiki wikis, including Bharatpedia.
  *
  *
  * The highlights include:
  * The highlights include:
Line 28: Line 28:
  *
  *
  * This library is maintained by the maintainers of Twinkle.
  * This library is maintained by the maintainers of Twinkle.
  * For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
  * For queries, suggestions, help, etc., head to [Bharatpedia talk:Twinkle on English Bharatpedia](http://en.wikipedia.org/wiki/WT:TW).
  * The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.js|GitHub}.
  * The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.js|GitHub}.
  *
  *
Line 40: Line 40:
var Morebits = {};
var Morebits = {};
window.Morebits = Morebits;  // allow global access
window.Morebits = Morebits;  // allow global access
/**
* i18n support for strings in Morebits
*/
Morebits.i18n = {
parser: null,
/**
* Set an i18n library to use with Morebits.
* Examples:
* Use jquery-i18n:
*    Morebits.i18n.setParser({ get: $.i18n });
* Use banana-i18n or orange-i18n:
*    var banana = new Banana('en');
*    Morebits.i18n.setParser({ get: banana.i18n });
* @param {Object} parser
*/
setParser: function(parser) {
if (!parser || typeof parser.get !== 'function') {
throw new Error('Morebits.i18n: parser must implement get()');
}
Morebits.i18n.parser = parser;
},
/**
* @private
* @returns {string}
*/
getMessage: function () {
var args = Array.prototype.slice.call(arguments); // array of size `n`
// 1st arg: message name
// 2nd to (n-1)th arg: message parameters
// nth arg: legacy English fallback
var msgName = args[0];
var fallback = args[args.length - 1];
if (!Morebits.i18n.parser) {
return fallback;
}
// i18n libraries are generally invoked with variable number of arguments
// as msg(msgName, ...parameters)
var i18nMessage = Morebits.i18n.parser.get.apply(null, args.slice(0, -1));
// if no i18n message exists, i18n libraries generally give back the message name
if (i18nMessage === msgName) {
return fallback;
}
return i18nMessage;
}
};
// shortcut
var msg = Morebits.i18n.getMessage;
/**
* Wiki-specific configurations for Morebits
*/
Morebits.l10n = {
/**
* Local aliases for "redirect" magic word.
* Check using api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=magicwords
*/
redirectTagAliases: ['#REDIRECT'],
/**
* Takes a string as argument and checks if it is a timestamp or not
* If not, it returns null. If yes, it returns an array of integers
* in the format [year, month, date, hour, minute, second]
* which can be passed to Date.UTC()
* @param {string} str
* @returns {number[] | null}
*/
signatureTimestampFormat: function (str) {
// HH:mm, DD Month YYYY (UTC)
var rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
var match = rgx.exec(str);
if (!match) {
return null;
}
var month = Morebits.date.localeData.months.indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
};




Line 63: Line 147:
  * Converts an IPv6 address to the canonical form stored and used by MediaWiki.
  * Converts an IPv6 address to the canonical form stored and used by MediaWiki.
  * JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
  * JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
  * function from the IPUtils library.  Adddresses are verbose, uppercase,
  * function from the IPUtils library.  Addresses are verbose, uppercase,
  * normalized, and expanded to 8 words.
  * normalized, and expanded to 8 words.
  *
  *
Line 77: Line 161:
  * Determines whether the current page is a redirect or soft redirect. Fails
  * Determines whether the current page is a redirect or soft redirect. Fails
  * to detect soft redirects on edit, history, etc. pages.  Will attempt to
  * to detect soft redirects on edit, history, etc. pages.  Will attempt to
  * detect [[Module:Redirect for discussion]], with the same failure points.
  * detect Module:RfD, with the same failure points.
  *
  *
  * @returns {boolean}
  * @returns {boolean}
  */
  */
Morebits.isPageRedirect = function() {
Morebits.isPageRedirect = function() {
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-Redirect_for_discussion').length);
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length);
};
};


Line 115: Line 199:


/**
/**
  * Create a string for use in regex matching all namespace aliases, regardless
* Converts string or array of DOM nodes into an HTML fragment.
  * of the capitalization and underscores/spaces.  Doesn't include the optional
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
  * leading `:`, but if there's more than one item, wraps the list in a
* Used in Morebits.quickForm and Morebits.status
  * non-capturing group.  This means you can do `Morebits.namespaceRegex([4]) +
* @internal
* @param {string|Node|(string|Node)[]} input
* @returns {DocumentFragment}
*/
Morebits.createHtml = function(input) {
var fragment = document.createDocumentFragment();
if (!input) {
return fragment;
}
if (!Array.isArray(input)) {
input = [ input ];
}
for (var i = 0; i < input.length; ++i) {
if (input[i] instanceof Node) {
fragment.appendChild(input[i]);
} else {
$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach(function(node) {
fragment.appendChild(node);
});
}
}
return fragment;
};
 
/**
* Converts wikilinks to HTML anchor tags.
* @param text
* @returns {*}
*/
Morebits.createHtml.renderWikilinks = function (text) {
var ub = new Morebits.unbinder(text);
// Don't convert wikilinks within code tags as they're used for displaying wiki-code
ub.unbind('<code>', '</code>');
ub.content = ub.content.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
function(_, target, text) {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '&#34;') + '">' + text + '</a>';
});
return ub.rebind();
};
 
/**
  * Create a string for use in regex matching all namespace aliases, regardless
  * of the capitalization and underscores/spaces.  Doesn't include the optional
  * leading `:`, but if there's more than one item, wraps the list in a
  * non-capturing group.  This means you can do `Morebits.namespaceRegex([4]) +
  * ':' + Morebits.pageNameRegex('Twinkle')` to match a full page.  Uses
  * ':' + Morebits.pageNameRegex('Twinkle')` to match a full page.  Uses
  * {@link Morebits.pageNameRegex}.
  * {@link Morebits.pageNameRegex}.
Line 137: Line 270:
if (namespaces.indexOf(number) !== -1) {
if (namespaces.indexOf(number) !== -1) {
// Namespaces are completely agnostic as to case,
// Namespaces are completely agnostic as to case,
// and a regex string is more useful/compatibile than a RegExp object,
// and a regex string is more useful/compatible than a RegExp object,
// so we accept any casing for any letter.
// so we accept any casing for any letter.
aliases.push(name.split('').map(function(char) {
aliases.push(name.split('').map(function(char) {
Line 201: Line 334:
  *
  *
  * Index to Morebits.quickForm.element types:
  * Index to Morebits.quickForm.element types:
  * - Global attributes: id, className, style, tooltip, extra, adminonly
  * - Global attributes: id, className, style, tooltip, extra, $data, adminonly
  * - `select`: A combo box (aka drop-down).
  * - `select`: A combo box (aka drop-down).
  *    - Attributes: name, label, multiple, size, list, event, disabled
  *    - Attributes: name, label, multiple, size, list, event, disabled
Line 236: Line 369:
  *  - `fragment`: A DocumentFragment object.
  *  - `fragment`: A DocumentFragment object.
  *      - No attributes, and no global attributes except adminonly.
  *      - No attributes, and no global attributes except adminonly.
* There is some difference on how types handle the `label` attribute:
* - `div`, `select`, `field`, `checkbox`/`radio`, `input`, `textarea`, `header`, and `dyninput` can accept an array of items,
* and the label item(s) can be `Element`s.
* - `option`, `optgroup`, `_dyninput_element`, `submit`, and `button` accept only a single string.
  *
  *
  * @memberof Morebits.quickForm
  * @memberof Morebits.quickForm
Line 253: Line 390:
this.data = data;
this.data = data;
this.childs = [];
this.childs = [];
this.id = Morebits.quickForm.element.id++;
};
};


Line 297: Line 433:
return currentNode[0];
return currentNode[0];
};
};


/** @memberof Morebits.quickForm.element */
/** @memberof Morebits.quickForm.element */
Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute(data, in_id) {
Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute(data, in_id) {
var node;
var node;
var childContainder = null;
var childContainer = null;
var label;
var label;
var id = (in_id ? in_id + '_' : '') + 'node_' + this.id;
var id = (in_id ? in_id + '_' : '') + 'node_' + Morebits.quickForm.element.id++;
if (data.adminonly && !Morebits.userIsSysop) {
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
// hell hack alpha
Line 330: Line 467:
label = node.appendChild(document.createElement('label'));
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.setAttribute('for', id);
label.appendChild(document.createTextNode(data.label));
label.appendChild(Morebits.createHtml(data.label));
label.style.marginRight = '3px';
}
}
var select = node.appendChild(document.createElement('select'));
var select = node.appendChild(document.createElement('select'));
Line 362: Line 500:
}
}
}
}
childContainder = select;
childContainer = select;
break;
break;
case 'option':
case 'option':
Line 395: Line 533:
node = document.createElement('fieldset');
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label = node.appendChild(document.createElement('legend'));
label.appendChild(document.createTextNode(data.label));
label.appendChild(Morebits.createHtml(data.label));
if (data.name) {
if (data.name) {
node.setAttribute('name', data.name);
node.setAttribute('name', data.name);
Line 442: Line 580:
}
}
label = cur_div.appendChild(document.createElement('label'));
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(current.label));
 
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
label.setAttribute('for', cur_id);
if (current.tooltip) {
if (current.tooltip) {
Line 528: Line 667:
if (data.label) {
if (data.label) {
label = node.appendChild(document.createElement('label'));
label = node.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(data.label));
label.appendChild(Morebits.createHtml(data.label));
label.setAttribute('for', data.id || id);
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
}


Line 560: Line 700:
}
}


childContainder = subnode;
childContainer = subnode;
break;
break;
case 'dyninput':
case 'dyninput':
Line 569: Line 709:


label = node.appendChild(document.createElement('h5'));
label = node.appendChild(document.createElement('h5'));
label.appendChild(document.createTextNode(data.label));
label.appendChild(Morebits.createHtml(data.label));
 
var listNode = node.appendChild(document.createElement('div'));
var listNode = node.appendChild(document.createElement('div'));


Line 622: Line 761:
label.appendChild(document.createTextNode(data.label));
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id);
label.setAttribute('for', id);
label.style.marginRight = '3px';
}
}


Line 670: Line 810:
case 'header':
case 'header':
node = document.createElement('h5');
node = document.createElement('h5');
node.appendChild(document.createTextNode(data.label));
node.appendChild(Morebits.createHtml(data.label));
break;
break;
case 'div':
case 'div':
Line 678: Line 818:
}
}
if (data.label) {
if (data.label) {
if (!Array.isArray(data.label)) {
data.label = [ data.label ];
}
var result = document.createElement('span');
var result = document.createElement('span');
result.className = 'quickformDescription';
result.className = 'quickformDescription';
for (i = 0; i < data.label.length; ++i) {
result.appendChild(Morebits.createHtml(data.label));
if (typeof data.label[i] === 'string') {
result.appendChild(document.createTextNode(data.label[i]));
} else if (data.label[i] instanceof Element) {
result.appendChild(data.label[i]);
}
}
node.appendChild(result);
node.appendChild(result);
}
}
Line 695: Line 826:
case 'submit':
case 'submit':
node = document.createElement('span');
node = document.createElement('span');
childContainder = node.appendChild(document.createElement('input'));
childContainer = node.appendChild(document.createElement('input'));
childContainder.setAttribute('type', 'submit');
childContainer.setAttribute('type', 'submit');
if (data.label) {
if (data.label) {
childContainder.setAttribute('value', data.label);
childContainer.setAttribute('value', data.label);
}
}
childContainder.setAttribute('name', data.name || 'submit');
childContainer.setAttribute('name', data.name || 'submit');
if (data.disabled) {
if (data.disabled) {
childContainder.setAttribute('disabled', 'disabled');
childContainer.setAttribute('disabled', 'disabled');
}
}
break;
break;
case 'button':
case 'button':
node = document.createElement('span');
node = document.createElement('span');
childContainder = node.appendChild(document.createElement('input'));
childContainer = node.appendChild(document.createElement('input'));
childContainder.setAttribute('type', 'button');
childContainer.setAttribute('type', 'button');
if (data.label) {
if (data.label) {
childContainder.setAttribute('value', data.label);
childContainer.setAttribute('value', data.label);
}
}
childContainder.setAttribute('name', data.name);
childContainer.setAttribute('name', data.name);
if (data.disabled) {
if (data.disabled) {
childContainder.setAttribute('disabled', 'disabled');
childContainer.setAttribute('disabled', 'disabled');
}
}
if (data.event) {
if (data.event) {
childContainder.addEventListener('click', data.event, false);
childContainer.addEventListener('click', data.event, false);
}
}
break;
break;
Line 726: Line 857:
label = node.appendChild(document.createElement('h5'));
label = node.appendChild(document.createElement('h5'));
var labelElement = document.createElement('label');
var labelElement = document.createElement('label');
labelElement.textContent = data.label;
labelElement.appendChild(Morebits.createHtml(data.label));
labelElement.setAttribute('for', data.id || id);
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
label.appendChild(labelElement);
Line 750: Line 881:
subnode.value = data.value;
subnode.value = data.value;
}
}
childContainder = subnode;
childContainer = subnode;
break;
break;
default:
default:
Line 756: Line 887:
}
}


if (!childContainder) {
if (!childContainer) {
childContainder = node;
childContainer = node;
}
}
if (data.tooltip) {
if (data.tooltip) {
Line 764: Line 895:


if (data.extra) {
if (data.extra) {
childContainder.extra = data.extra;
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
}
if (data.style) {
if (data.style) {
childContainder.setAttribute('style', data.style);
childContainer.setAttribute('style', data.style);
}
}
if (data.className) {
if (data.className) {
childContainder.className = childContainder.className ?
childContainer.className = childContainer.className ?
childContainder.className + ' ' + data.className :
childContainer.className + ' ' + data.className :
data.className;
data.className;
}
}
childContainder.setAttribute('id', data.id || id);
childContainer.setAttribute('id', data.id || id);


return [ node, childContainder ];
return [ node, childContainer ];
};
};


Line 791: Line 925:
tooltipButton.className = 'morebits-tooltipButton';
tooltipButton.className = 'morebits-tooltipButton';
tooltipButton.title = data.tooltip; // Provides the content for jQuery UI
tooltipButton.title = data.tooltip; // Provides the content for jQuery UI
tooltipButton.appendChild(document.createTextNode('?'));
tooltipButton.appendChild(document.createTextNode(msg('tooltip-mark', '?')));
$(tooltipButton).tooltip({
$(tooltipButton).tooltip({
position: { my: 'left top', at: 'center bottom', collision: 'flipfit' },
position: { my: 'left top', at: 'center bottom', collision: 'flipfit' },
Line 1,148: Line 1,282:
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* function from the IPUtils library.  Adddresses are verbose, uppercase,
* function from the IPUtils library.  Addresses are verbose, uppercase,
* normalized, and expanded to 8 words.
* normalized, and expanded to 8 words.
*
*
Line 1,213: Line 1,347:
/**
/**
* Check that an IP range is within the CIDR limits.  Most likely to be useful
* Check that an IP range is within the CIDR limits.  Most likely to be useful
* in conjunction with `wgRelevantUserName`.  CIDR limits are harcoded as /16
* in conjunction with `wgRelevantUserName`.  CIDR limits are hardcoded as /16
* for IPv4 and /32 for IPv6.
* for IPv4 and /32 for IPv6.
*
*
Line 1,256: Line 1,390:
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
}
};
/**
* @external RegExp
*/
/**
* Deprecated as of September 2020, use {@link Morebits.string.escapeRegExp}
* or `mw.util.escapeRegExp`.
*
* @function external:RegExp.escape
* @deprecated Use {@link Morebits.string.escapeRegExp} or `mw.util.escapeRegExp`.
* @param {string} text - String to be escaped.
* @param {boolean} [space_fix=false] - Whether to replace spaces and
* underscores with `[ _]` as they are often equivalent.
* @returns {string} - The escaped text.
*/
RegExp.escape = function(text, space_fix) {
if (space_fix) {
console.error('NOTE: RegExp.escape from Morebits was deprecated September 2020, please replace it with Morebits.string.escapeRegExp'); // eslint-disable-line no-console
return Morebits.string.escapeRegExp(text);
}
console.error('NOTE: RegExp.escape from Morebits was deprecated September 2020, please replace it with mw.util.escapeRegExp'); // eslint-disable-line no-console
return mw.util.escapeRegExp(text);
};
};


Line 1,435: Line 1,545:
* Escapes a string to be used in a RegExp, replacing spaces and
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
* underscores with `[_ ]` as they are often equivalent.
* Replaced RegExp.escape September 2020.
*
*
* @param {string} text - String to be escaped.
* @param {string} text - String to be escaped.
Line 1,591: Line 1,700:
var search = target.data('select2').dropdown.$search ||
var search = target.data('select2').dropdown.$search ||
target.data('select2').selection.$search;
target.data('select2').selection.$search;
search.focus();
// Use DOM .focus() to work around a jQuery 3.6.0 regression (https://github.com/select2/select2/issues/5993)
search[0].focus();
}
}


Line 1,699: Line 1,809:
} else if (typeof param === 'string') {
} else if (typeof param === 'string') {
// Wikitext signature timestamp
// Wikitext signature timestamp
var dateParts = Morebits.date.localeData.signatureTimestampFormat(param);
var dateParts = Morebits.l10n.signatureTimestampFormat(param);
if (dateParts) {
if (dateParts) {
this._d = new Date(Date.UTC.apply(null, dateParts));
this._d = new Date(Date.UTC.apply(null, dateParts));
Line 1,730: Line 1,840:
  */
  */
Morebits.date.localeData = {
Morebits.date.localeData = {
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
// message names here correspond to MediaWiki message names
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
months: [msg('january', 'January'), msg('february', 'February'), msg('march', 'March'),
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
msg('april', 'April'), msg('may_long', 'May'), msg('june', 'June'),
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
msg('july', 'July'), msg('august', 'August'), msg('september', 'September'),
msg('october', 'October'), msg('november', 'November'), msg('december', 'December')],
monthsShort: [msg('jan', 'Jan'), msg('feb', 'Feb'), msg('mar', 'Mar'),
msg('apr', 'Apr'), msg('may', 'May'), msg('jun', 'Jun'),
msg('jul', 'Jul'), msg('aug', 'Aug'), msg('sep', 'Sep'),
msg('oct', 'Oct'), msg('nov', 'Nov'), msg('dec', 'Dec')],
days: [msg('sunday', 'Sunday'), msg('monday', 'Monday'), msg('tuesday', 'Tuesday'),
msg('wednesday', 'Wednesday'), msg('thursday', 'Thursday'), msg('friday', 'Friday'),
msg('saturday', 'Saturday')],
daysShort: [msg('sun', 'Sun'), msg('mon', 'Mon'), msg('tue', 'Tue'),
msg('wed', 'Wed'), msg('thu', 'Thu'), msg('fri', 'Fri'),
msg('sat', 'Sat')],
 
relativeTimes: {
relativeTimes: {
thisDay: '[Today at] h:mm A',
thisDay: msg('relative-today', '[Today at] h:mm A'),
prevDay: '[Yesterday at] h:mm A',
prevDay: msg('relative-prevday', '[Yesterday at] h:mm A'),
nextDay: '[Tomorrow at] h:mm A',
nextDay: msg('relative-nextday', '[Tomorrow at] h:mm A'),
thisWeek: 'dddd [at] h:mm A',
thisWeek: msg('relative-thisweek', 'dddd [at] h:mm A'),
pastWeek: '[Last] dddd [at] h:mm A',
pastWeek: msg('relative-pastweek', '[Last] dddd [at] h:mm A'),
other: 'YYYY-MM-DD'
other: msg('relative-other', 'YYYY-MM-DD')
},
signatureTimestampFormat: function (str) {
// HH:mm, DD Month YYYY (UTC)
var rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
var match = rgx.exec(str);
if (!match) {
return null;
}
var month = Morebits.date.localeData.months.indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
}
};
};
Line 1,885: Line 1,993:
* |--------|--------|
* |--------|--------|
* | H | Hours (24-hour) |
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | A | AM or PM |
* | m | Minutes |
* | m | Minutes |
* | mm | Minutes (padded) |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | s | Seconds |
* | ss | Seconds (padded) |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment, padded |
* | SSS | Milliseconds fragment, 3 digits |
* | d | Day number of the week (Sun=0) |
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | dddd | Full day name |
* | D | Date |
* | D | Date |
* | DD | Date (padded) |
* | DD | Date (padded to 2 digits) |
* | M | Month number (0-indexed) |
* | M | Month number (1-indexed) |
* | MM | Month number (0-indexed, padded) |
* | MM | Month number (1-indexed, padded to 2 digits) |
* | MMM | Abbreviated month name |
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
* | MMMM | Full month name |
Line 1,938: Line 2,046:
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
var D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
var D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? 'PM' : 'AM';
var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? msg('period-pm', 'PM') : msg('period-am', 'AM');
var replacementMap = {
var replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
Line 2,297: Line 2,405:
this.onSuccess.call(this.parent, this);
this.onSuccess.call(this.parent, this);
} else {
} else {
this.statelem.info('done');
this.statelem.info(msg('done', 'done'));
}
}


Line 2,309: Line 2,417:
this.statusText = statusText;
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
this.errorText = msg('api-error', statusText, jqXHR.statusText, statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.');
return this.returnError();
return this.returnError();
}
}
Line 2,318: Line 2,426:
returnError: function(callerAjaxParameters) {
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn('Invalid token. Getting a new token and retrying...');
this.statelem.warn(msg('invalid-token-retrying', 'Invalid token. Getting a new token and retrying...'));
this.badtokenRetry = true;
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
// Get a new CSRF token and retry. If the original action needs a different
Line 2,362: Line 2,470:
}
}


};
/** Retrieves wikitext from a page. Caching enabled, duration 1 day. */
Morebits.wiki.getCachedJson = function(title) {
var query = {
action: 'query',
prop: 'revisions',
titles: title,
rvslots: '*',
rvprop: 'content',
format: 'json',
smaxage: '86400', // cache for 1 day
maxage: '86400' // cache for 1 day
};
return new Morebits.wiki.api('', query).post().then(function(apiobj) {
apiobj.getStatusElement().unlink();
var response = apiobj.getResponse();
var wikitext = response.query.pages[0].revisions[0].slots.main.content;
return JSON.parse(wikitext);
});
};
};


Line 2,402: Line 2,530:
  */
  */
Morebits.wiki.api.getToken = function() {
Morebits.wiki.api.getToken = function() {
var tokenApi = new Morebits.wiki.api('Getting token', {
var tokenApi = new Morebits.wiki.api(msg('getting-token', 'Getting token'), {
action: 'query',
action: 'query',
meta: 'tokens',
meta: 'tokens',
Line 2,465: Line 2,593:


if (!status) {
if (!status) {
status = 'Opening page "' + pageName + '"';
status = msg('opening-page', pageName, 'Opening page "' + pageName + '"');
}
}


Line 2,632: Line 2,760:
}
}


ctx.loadApi = new Morebits.wiki.api('Retrieving page...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi = new Morebits.wiki.api(msg('retrieving-page', 'Retrieving page...'), ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
ctx.loadApi.post();
Line 2,638: Line 2,766:


/**
/**
* Saves the text for the page to Wikipedia.
* Saves the text for the page to Bharatpedia.
* Must be preceded by successfully calling `load()`.
* Must be preceded by successfully calling `load()`.
*
*
Line 2,644: Line 2,772:
* previous `load()` callbacks to recover from edit conflicts! In this
* previous `load()` callbacks to recover from edit conflicts! In this
* case, callers must make the same edit to the new pageText and
* case, callers must make the same edit to the new pageText and
* reinvoke `save()`.  This behavior can be disabled with
* re-invoke `save()`.  This behavior can be disabled with
* `setMaxConflictRetries(0)`.
* `setMaxConflictRetries(0)`.
*
*
Line 2,677: Line 2,805:
// shouldn't happen if canUseMwUserToken === true
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm('You are about to make an edit to the fully protected page "' + ctx.pageName +
!confirm(
(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(ctx.fullyProtected).calendar('utc') + ' (UTC))') +
ctx.fullyProtected === 'infinity'
'.  \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) {
? msg('protected-indef-edit-warning', ctx.pageName,
ctx.statusElement.error('Edit to fully protected page was aborted.');
'You are about to make an edit to the fully protected page "' + ctx.pageName + '" (protected indefinitely).  \n\nClick OK to proceed with the edit, or Cancel to skip this edit.'
)
: msg('protected-edit-warning', ctx.pageName, ctx.fullyProtected,
'You are about to make an edit to the fully protected page "' + ctx.pageName +
'" (protection expiring ' + new Morebits.date(ctx.fullyProtected).calendar('utc') + ' (UTC)).  \n\nClick OK to proceed with the edit, or Cancel to skip this edit.'
)
)
) {
ctx.statusElement.error(msg('protected-aborted', 'Edit to fully protected page was aborted.'));
ctx.onSaveFailure(this);
ctx.onSaveFailure(this);
return;
return;
Line 2,714: Line 2,850:
}
}


// Set bot edit attribute. If this paramter is present with any value, it is interpreted as true
// Set bot edit attribute. If this parameter is present with any value, it is interpreted as true
if (ctx.botEdit) {
if (ctx.botEdit) {
query.bot = true;
query.bot = true;
Line 2,771: Line 2,907:
}
}


ctx.saveApi = new Morebits.wiki.api('Saving page...', query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi = new Morebits.wiki.api(msg('saving-page', 'Saving page...'), query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
ctx.saveApi.post();
Line 3,104: Line 3,240:
* 1. If there are no revisions among the first 50 that are
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrived.
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* (revdeled/suppressed) will be treated as non-redirects.
Line 3,335: Line 3,471:
}
}


ctx.lookupCreationApi = new Morebits.wiki.api('Retrieving page creation information', query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi = new Morebits.wiki.api(msg('getting-creator', 'Retrieving page creation information'), query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
ctx.lookupCreationApi.post();
Line 3,385: Line 3,521:
var query = fnNeedTokenInfoQuery('move');
var query = fnNeedTokenInfoQuery('move');


ctx.moveApi = new Morebits.wiki.api('retrieving token...', query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
ctx.moveApi.post();
Line 3,424: Line 3,560:
};
};


ctx.patrolApi = new Morebits.wiki.api('retrieving token...', patrolQuery, fnProcessPatrol);
ctx.patrolApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
ctx.patrolApi.post();
Line 3,463: Line 3,599:
var query = fnNeedTokenInfoQuery('triage');
var query = fnNeedTokenInfoQuery('triage');


ctx.triageApi = new Morebits.wiki.api('retrieving token...', query, fnProcessTriageList);
ctx.triageApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
ctx.triageApi.post();
Line 3,490: Line 3,626:
var query = fnNeedTokenInfoQuery('delete');
var query = fnNeedTokenInfoQuery('delete');


ctx.deleteApi = new Morebits.wiki.api('retrieving token...', query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
ctx.deleteApi.post();
Line 3,515: Line 3,651:
var query = fnNeedTokenInfoQuery('undelete');
var query = fnNeedTokenInfoQuery('undelete');


ctx.undeleteApi = new Morebits.wiki.api('retrieving token...', query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
ctx.undeleteApi.post();
Line 3,546: Line 3,682:
var query = fnNeedTokenInfoQuery('protect');
var query = fnNeedTokenInfoQuery('protect');


ctx.protectApi = new Morebits.wiki.api('retrieving token...', query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
ctx.protectApi.post();
Line 3,581: Line 3,717:
var query = fnNeedTokenInfoQuery('stabilize');
var query = fnNeedTokenInfoQuery('stabilize');


ctx.stabilizeApi = new Morebits.wiki.api('retrieving token...', query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
ctx.stabilizeApi.post();
Line 3,704: Line 3,840:
ctx.csrfToken = response.tokens.csrftoken;
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
if (!ctx.csrfToken) {
ctx.statusElement.error('Failed to retrieve edit token.');
ctx.statusElement.error(msg('token-fetch-fail', 'Failed to retrieve edit token.'));
ctx.onLoadFailure(this);
ctx.onLoadFailure(this);
return;
return;
Line 3,778: Line 3,914:
// check for invalid titles
// check for invalid titles
if (page.invalid) {
if (page.invalid) {
ctx.statusElement.error('The page title is invalid: ' + ctx.pageName);
ctx.statusElement.error(msg('invalid-title', ctx.pageName, 'The page title is invalid: ' + ctx.pageName));
onFailure(this);
onFailure(this);
return false; // abort
return false; // abort
Line 3,791: Line 3,927:
var newNs = new mw.Title(resolvedName).namespace;
var newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(ctx.pageName + ' is a cross-namespace redirect to ' + resolvedName + ', aborted');
ctx.statusElement.error(msg('cross-redirect-abort', ctx.pageName, resolvedName, ctx.pageName + ' is a cross-namespace redirect to ' + resolvedName + ', aborted'));
onFailure(this);
onFailure(this);
return false;
return false;
Line 3,797: Line 3,933:


// only notify user for redirects, not normalization
// only notify user for redirects, not normalization
new Morebits.status('Note', 'Redirected from ' + ctx.pageName + ' to ' + resolvedName);
new Morebits.status('Note', msg('redirected', ctx.pageName, resolvedName, 'Redirected from ' + ctx.pageName + ' to ' + resolvedName));
}
}


Line 3,804: Line 3,940:
} else {
} else {
// could be a circular redirect or other problem
// could be a circular redirect or other problem
ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
ctx.statusElement.error(msg('redirect-resolution-fail', ctx.pageName, 'Could not resolve redirects for: ' + ctx.pageName));
onFailure(this);
onFailure(this);


Line 3,881: Line 4,017:
ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
} else {
} else {
ctx.statusElement.error('Unknown error received from API while saving page');
ctx.statusElement.error(msg('api-error-unknown', 'Unknown error received from API while saving page'));
}
}


Line 3,903: Line 4,039:
};
};


var purgeApi = new Morebits.wiki.api('Edit conflict detected, purging server cache', purgeQuery, function() {
var purgeApi = new Morebits.wiki.api(msg('editconflict-purging', 'Edit conflict detected, purging server cache'), purgeQuery, function() {
--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(msg('editconflict-retrying', 'Edit conflict detected, reapplying edit'));
if (fnCanUseMwUserToken('edit')) {
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
Line 3,919: Line 4,055:


// the error might be transient, so try again
// the error might be transient, so try again
ctx.statusElement.info('Save failed, retrying in 2 seconds ...');
ctx.statusElement.info(msg('save-failed-retrying', 2, 'Save failed, retrying in 2 seconds ...'));
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds
--Morebits.wiki.numberOfActionsLeft;  // allow for normal completion if retry succeeds


// wait for sometime for client to regain connnectivity
// wait for sometime for client to regain connectivity
sleep(2000).then(function() {
sleep(2000).then(function() {
ctx.saveApi.post(); // give it another go!
ctx.saveApi.post(); // give it another go!
Line 3,962: Line 4,098:
}
}
}
}
};
var isTextRedirect = function(text) {
if (!text) { // no text - content empty or inaccessible (revdelled or suppressed)
return false;
}
return Morebits.l10n.redirectTagAliases.some(function(tag) {
return new RegExp('^\\s*' + tag + '\\W', 'i').test(text);
});
};
};


Line 3,976: Line 4,121:
ctx.onLookupCreationFailure(this);
ctx.onLookupCreationFailure(this);
return;
return;
}
}
 
 
if (!ctx.lookupNonRedirectCreator || !/^\s*#redirect/i.test(rev.content)) {
if (!ctx.lookupNonRedirectCreator || !isTextRedirect(rev.content)) {
 
ctx.creator = rev.user;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
 
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
 
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
 
ctx.lookupCreationApi = new Morebits.wiki.api('Retrieving page creation information', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
 
};
 
var fnLookupNonRedirectCreator = function() {
var response = ctx.lookupCreationApi.getResponse().query;
var revs = response.pages[0].revisions;
 
for (var i = 0; i < revs.length; i++) {


ctx.creator = rev.user;
if (!isTextRedirect(revs[i].content)) {
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
 
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
 
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
 
ctx.lookupCreationApi = new Morebits.wiki.api('Retrieving page creation information', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
 
};
 
var fnLookupNonRedirectCreator = function() {
var response = ctx.lookupCreationApi.getResponse().query;
var revs = response.pages[0].revisions;
 
for (var i = 0; i < revs.length; i++) {
if (!/^\s*#redirect/i.test(revs[i].content)) { // inaccessible revisions also check out
ctx.creator = revs[i].user;
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
ctx.timestamp = revs[i].timestamp;
Line 4,162: Line 4,308:
}
}


ctx.moveProcessApi = new Morebits.wiki.api('moving page...', query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi = new Morebits.wiki.api(msg('moving-page', 'moving page...'), query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
ctx.moveProcessApi.post();
Line 4,775: Line 4,921:
*/
*/
removeLink: function(link_target) {
removeLink: function(link_target) {
// Rempve a leading colon, to be handled later
// Remove a leading colon, to be handled later
if (link_target.indexOf(':') === 0) {
if (link_target.indexOf(':') === 0) {
link_target = link_target.slice(1);
link_target = link_target.slice(1);
Line 4,837: Line 4,983:
unbinder.unbind('<!--', '-->');
unbinder.unbind('<!--', '-->');


// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceeded by an |
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceeding bar and an eventual named parameter
// Will only eat the image name and the preceding bar and an eventual named parameter
var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
Line 5,048: Line 5,194:
Morebits.status = function Status(text, stat, type) {
Morebits.status = function Status(text, stat, type) {
this.textRaw = text;
this.textRaw = text;
this.text = this.codify(text);
this.text = Morebits.createHtml(text);
this.type = type || 'status';
this.type = type || 'status';
this.generate();
this.generate();
Line 5,113: Line 5,259:
this.linked = false;
this.linked = false;
}
}
},
/**
* Create a document fragment with the status text, parsing as HTML.
* Runs upon construction for text (part before colon) and upon
* render/update for status (part after colon).
*
* @param {(string|Element|Array)} obj
* @returns {DocumentFragment}
*/
codify: function(obj) {
if (!Array.isArray(obj)) {
obj = [ obj ];
}
var result;
result = document.createDocumentFragment();
for (var i = 0; i < obj.length; ++i) {
if (obj[i] instanceof Element) {
result.appendChild(obj[i]);
} else {
$.parseHTML(obj[i]).forEach(function(elem) {
result.appendChild(elem);
});
}
}
return result;
},
},


Line 5,151: Line 5,270:
update: function(status, type) {
update: function(status, type) {
this.statRaw = status;
this.statRaw = status;
this.stat = this.codify(status);
this.stat = Morebits.createHtml(status);
if (type) {
if (type) {
this.type = type;
this.type = type;
Line 5,402: Line 5,521:


// internal counters, etc.
// internal counters, etc.
statusElement: new Morebits.status(currentAction || 'Performing batch operation'),
statusElement: new Morebits.status(currentAction || msg('batch-starting', 'Performing batch operation')),
worker: null, // function that executes for each item in pageList
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
postFinish: null, // function that executes when the whole batch has been processed
Line 5,469: Line 5,588:
var total = ctx.pageList.length;
var total = ctx.pageList.length;
if (!total) {
if (!total) {
ctx.statusElement.info('no pages specified');
ctx.statusElement.info(msg('batch-no-pages', 'no pages specified'));
ctx.running = false;
ctx.running = false;
if (ctx.postFinish) {
if (ctx.postFinish) {
Line 5,487: Line 5,606:


/**
/**
* To be called by worker before it terminates succesfully.
* To be called by worker before it terminates successfully.
*
*
* @param {(Morebits.wiki.page|Morebits.wiki.api|string)} arg -
* @param {(Morebits.wiki.page|Morebits.wiki.api|string)} arg -
Line 5,496: Line 5,615:
*/
*/
this.workerSuccess = function(arg) {
this.workerSuccess = function(arg) {
var createPageLink = function(pageName) {
var link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(pageName));
link.appendChild(document.createTextNode(pageName));
return link;
};


if (arg instanceof Morebits.wiki.api || arg instanceof Morebits.wiki.page) {
if (arg instanceof Morebits.wiki.api || arg instanceof Morebits.wiki.page) {
Line 5,511: Line 5,623:
// we know the page title - display a relevant message
// we know the page title - display a relevant message
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info(['completed (', createPageLink(pageName), ')']);
statelem.info(msg('batch-done-page', pageName, 'completed ([[' + pageName + ']])'));
} else {
} else {
// we don't know the page title - just display a generic message
// we don't know the page title - just display a generic message
statelem.info('done');
statelem.info(msg('done', 'done'));
}
}
} else {
} else {
Line 5,522: Line 5,634:


} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.status(arg, ['done (', createPageLink(arg), ')']);
new Morebits.status(arg, msg('batch-done-page', arg, 'completed ([[' + arg + ']])'));
}
}


Line 5,556: Line 5,668:
var total = ctx.pageList.length;
var total = ctx.pageList.length;
if (ctx.countFinished < total) {
if (ctx.countFinished < total) {
ctx.statusElement.status(parseInt(100 * ctx.countFinished / total, 10) + '%');
var progress = Math.round(100 * ctx.countFinished / total);
ctx.statusElement.status(msg('percent', progress, progress + '%'));


// start a new chunk if we're close enough to the end of the previous chunk, and
// start a new chunk if we're close enough to the end of the previous chunk, and
Line 5,565: Line 5,678:
}
}
} else if (ctx.countFinished === total) {
} else if (ctx.countFinished === total) {
var statusString = 'Done (' + ctx.countFinishedSuccess +
var statusString = msg('batch-progress', ctx.countFinishedSuccess, ctx.countFinished, 'Done (' + ctx.countFinishedSuccess +
'/' + ctx.countFinished + ' actions completed successfully)';
'/' + ctx.countFinished + ' actions completed successfully)');
if (ctx.countFinishedSuccess < ctx.countFinished) {
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
ctx.statusElement.warn(statusString);
Line 5,866: Line 5,979:
value.style.display = 'none';
value.style.display = 'none';
var button = document.createElement('button');
var button = document.createElement('button');
button.textContent = value.hasAttribute('value') ? value.getAttribute('value') : value.textContent ? value.textContent : 'Submit Query';
button.textContent = value.hasAttribute('value') ? value.getAttribute('value') : value.textContent ? value.textContent : msg('submit', 'Submit Query');
button.className = value.className || 'submitButtonProxy';
button.className = value.className || 'submitButtonProxy';
// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
Line 5,914: Line 6,027:
if (this.hasFooterLinks) {
if (this.hasFooterLinks) {
var bullet = document.createElement('span');
var bullet = document.createElement('span');
bullet.textContent = ' \u2022 ';  // U+2022 BULLET
bullet.textContent = msg('bullet-separator', ' \u2022 ');  // U+2022 BULLET
if (prep) {
if (prep) {
$footerlinks.prepend(bullet);
$footerlinks.prepend(bullet);
Line 5,982: Line 6,095:
window.SimpleWindow = Morebits.simpleWindow;
window.SimpleWindow = Morebits.simpleWindow;
window.QuickForm = Morebits.quickForm;
window.QuickForm = Morebits.quickForm;
window.Wikipedia = Morebits.wiki;
window.Bharatpedia = Morebits.wiki;
window.Status = Morebits.status;
window.Status = Morebits.status;
}
}


// </nowiki>
// </nowiki>