MediaWiki:Gadget-morebits.js: Difference between revisions
Content deleted Content added
Maintenance: mw:RL/MGU - Updated deprecated module name |
MusikAnimal (talk | contribs) Repo at 66e1c54: various bug fixes, localization features |
||
Line 40:
var Morebits = {};
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 112 ⟶ 196:
}
return Morebits.string.escapeRegExp(firstChar) + remainder;
};
/**
* Converts string or array of DOM nodes into an HTML fragment.
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
* Used in Morebits.quickForm and Morebits.status
* @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, '"') + '">' + text + '</a>';
});
return ub.rebind();
};
Line 201 ⟶ 334:
*
* Index to Morebits.quickForm.element types:
* - Global attributes: id, className, style, tooltip, extra, $data, adminonly
* - `select`: A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event, disabled
Line 236 ⟶ 369:
* - `fragment`: A DocumentFragment object.
* - 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
Line 253 ⟶ 390:
this.data = data;
this.childs = [];
};
Line 297 ⟶ 433:
return currentNode[0];
};
/** @memberof Morebits.quickForm.element */
Line 303 ⟶ 440:
var childContainer = null;
var label;
var id = (in_id ? in_id + '_' : '') + 'node_' +
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
Line 330 ⟶ 467:
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.appendChild(
label.style.marginRight = '3px';
}
var select = node.appendChild(document.createElement('select'));
Line 395 ⟶ 533:
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label.appendChild(
if (data.name) {
node.setAttribute('name', data.name);
Line 442 ⟶ 580:
}
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
if (current.tooltip) {
Line 528 ⟶ 667:
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
Line 569 ⟶ 709:
label = node.appendChild(document.createElement('h5'));
label.appendChild(
var listNode = node.appendChild(document.createElement('div'));
Line 622 ⟶ 761:
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id);
label.style.marginRight = '3px';
}
Line 670 ⟶ 810:
case 'header':
node = document.createElement('h5');
node.appendChild(
break;
case 'div':
Line 678 ⟶ 818:
}
if (data.label) {
var result = document.createElement('span');
result.className = 'quickformDescription';
node.appendChild(result);
}
Line 726 ⟶ 857:
label = node.appendChild(document.createElement('h5'));
var labelElement = document.createElement('label');
labelElement.
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
Line 765 ⟶ 896:
if (data.extra) {
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
if (data.style) {
Line 791 ⟶ 925:
tooltipButton.className = 'morebits-tooltipButton';
tooltipButton.title = data.tooltip; // Provides the content for jQuery UI
tooltipButton.appendChild(document.createTextNode(msg('tooltip-mark', '?')));
$(tooltipButton).tooltip({
position: { my: 'left top', at: 'center bottom', collision: 'flipfit' },
Line 1,256 ⟶ 1,390:
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
};
Line 1,435 ⟶ 1,545:
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
*
* @param {string} text - String to be escaped.
Line 1,591 ⟶ 1,700:
var search = target.data('select2').dropdown.$search ||
target.data('select2').selection.$search;
// 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 ⟶ 1,809:
} else if (typeof param === 'string') {
// Wikitext signature timestamp
var dateParts = Morebits.
if (dateParts) {
this._d = new Date(Date.UTC.apply(null, dateParts));
Line 1,730 ⟶ 1,840:
*/
Morebits.date.localeData = {
// message names here correspond to MediaWiki message names
months: [msg('january', 'January'), msg('february', 'February'), msg('march', 'March'),
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: {
thisDay: msg('relative-today', '[Today at] h:mm A'),
prevDay: msg('relative-prevday', '[Yesterday at] h:mm A'),
nextDay: msg('relative-nextday', '[Tomorrow at] h:mm A'),
thisWeek: msg('relative-thisweek', 'dddd [at] h:mm A'),
pastWeek: msg('relative-pastweek', '[Last] dddd [at] h:mm A'),
other: msg('relative-other', 'YYYY-MM-DD')
}
};
Line 1,885 ⟶ 1,993:
* |--------|--------|
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | m | Minutes |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment,
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | D | Date |
* | DD | Date (padded to 2 digits) |
* | M | Month number (
* | MM | Month number (
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
Line 1,938 ⟶ 2,046:
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 h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? msg('period-pm', 'PM') : msg('period-am', 'AM');
var replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
Line 2,297 ⟶ 2,405:
this.onSuccess.call(this.parent, this);
} else {
this.statelem.info(msg('done', 'done'));
}
Line 2,309 ⟶ 2,417:
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = msg('api-error', statusText, jqXHR.statusText, statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.');
return this.returnError();
}
Line 2,318 ⟶ 2,426:
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn(msg('invalid-token-retrying', 'Invalid token. Getting a new token and retrying...'));
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
Line 2,402 ⟶ 2,510:
*/
Morebits.wiki.api.getToken = function() {
var tokenApi = new Morebits.wiki.api(msg('getting-token', 'Getting token'), {
action: 'query',
meta: 'tokens',
Line 2,465 ⟶ 2,573:
if (!status) {
status = msg('opening-page', pageName, 'Opening page "' + pageName + '"');
}
Line 2,632 ⟶ 2,740:
}
ctx.loadApi = new Morebits.wiki.api(msg('retrieving-page', 'Retrieving page...'), ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
Line 2,677 ⟶ 2,785:
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm(
? msg('protected-indef-edit-warning', ctx.pageName,
'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);
return;
Line 2,771 ⟶ 2,887:
}
ctx.saveApi = new Morebits.wiki.api(msg('saving-page', 'Saving page...'), query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
Line 3,335 ⟶ 3,451:
}
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.post();
Line 3,385 ⟶ 3,501:
var query = fnNeedTokenInfoQuery('move');
ctx.moveApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
Line 3,424 ⟶ 3,540:
};
ctx.patrolApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
Line 3,463 ⟶ 3,579:
var query = fnNeedTokenInfoQuery('triage');
ctx.triageApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
Line 3,490 ⟶ 3,606:
var query = fnNeedTokenInfoQuery('delete');
ctx.deleteApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
Line 3,515 ⟶ 3,631:
var query = fnNeedTokenInfoQuery('undelete');
ctx.undeleteApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
Line 3,546 ⟶ 3,662:
var query = fnNeedTokenInfoQuery('protect');
ctx.protectApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
Line 3,581 ⟶ 3,697:
var query = fnNeedTokenInfoQuery('stabilize');
ctx.stabilizeApi = new Morebits.wiki.api(msg('getting-token', 'retrieving token...'), query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
Line 3,704 ⟶ 3,820:
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
ctx.statusElement.error(msg('token-fetch-fail', 'Failed to retrieve edit token.'));
ctx.onLoadFailure(this);
return;
Line 3,778 ⟶ 3,894:
// check for invalid titles
if (page.invalid) {
ctx.statusElement.error(msg('invalid-title', ctx.pageName, 'The page title is invalid: ' + ctx.pageName));
onFailure(this);
return false; // abort
Line 3,791 ⟶ 3,907:
var newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(msg('cross-redirect-abort', ctx.pageName, resolvedName, ctx.pageName + ' is a cross-namespace redirect to ' + resolvedName + ', aborted'));
onFailure(this);
return false;
Line 3,797 ⟶ 3,913:
// only notify user for redirects, not normalization
new Morebits.status('Note', msg('redirected', ctx.pageName, resolvedName, 'Redirected from ' + ctx.pageName + ' to ' + resolvedName));
}
Line 3,804 ⟶ 3,920:
} else {
// could be a circular redirect or other problem
ctx.statusElement.error(msg('redirect-resolution-fail', ctx.pageName, 'Could not resolve redirects for: ' + ctx.pageName));
onFailure(this);
Line 3,881 ⟶ 3,997:
ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
} else {
ctx.statusElement.error(msg('api-error-unknown', 'Unknown error received from API while saving page'));
}
Line 3,903 ⟶ 4,019:
};
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
ctx.statusElement.info(msg('editconflict-retrying', 'Edit conflict detected, reapplying edit'));
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
Line 3,919 ⟶ 4,035:
// the error might be transient, so try again
ctx.statusElement.info(msg('save-failed-retrying', 2, 'Save failed, retrying in 2 seconds ...'));
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
Line 3,962 ⟶ 4,078:
}
}
};
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,978 ⟶ 4,103:
}
if (!ctx.lookupNonRedirectCreator || !
ctx.creator = rev.user;
Line 4,012 ⟶ 4,137:
for (var i = 0; i < revs.length; i++) {
if (!isTextRedirect(revs[i].content)) {
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
Line 4,162 ⟶ 4,288:
}
ctx.moveProcessApi = new Morebits.wiki.api(msg('moving-page', 'moving page...'), query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
Line 5,048 ⟶ 5,174:
Morebits.status = function Status(text, stat, type) {
this.textRaw = text;
this.text =
this.type = type || 'status';
this.generate();
Line 5,113 ⟶ 5,239:
this.linked = false;
}
},
Line 5,151 ⟶ 5,250:
update: function(status, type) {
this.statRaw = status;
this.stat =
if (type) {
this.type = type;
Line 5,402 ⟶ 5,501:
// internal counters, etc.
statusElement: new Morebits.status(currentAction || msg('batch-starting', 'Performing batch operation')),
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
Line 5,469 ⟶ 5,568:
var total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info(msg('batch-no-pages', 'no pages specified'));
ctx.running = false;
if (ctx.postFinish) {
Line 5,496 ⟶ 5,595:
*/
this.workerSuccess = function(arg) {
if (arg instanceof Morebits.wiki.api || arg instanceof Morebits.wiki.page) {
Line 5,511 ⟶ 5,603:
// we know the page title - display a relevant message
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info(
} else {
// we don't know the page title - just display a generic message
statelem.info(msg('done', 'done'));
}
} else {
Line 5,522 ⟶ 5,614:
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.status(arg,
}
Line 5,556 ⟶ 5,648:
var total = ctx.pageList.length;
if (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
Line 5,565 ⟶ 5,658:
}
} else if (ctx.countFinished === total) {
var statusString = msg('batch-progress', ctx.countFinishedSuccess, ctx.countFinished, 'Done (' + ctx.countFinishedSuccess +
'/' + ctx.countFinished + ' actions completed successfully)');
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
Line 5,662 ⟶ 5,755:
* @memberof Morebits
* @class
* @requires jquery.ui.dialog
* @param {number} width
* @param {number} height - The maximum allowable height for the content area.
Line 5,866 ⟶ 5,959:
value.style.display = 'none';
var button = document.createElement('button');
button.textContent = value.hasAttribute('value') ? value.getAttribute('value') : value.textContent ? value.textContent : msg('submit', 'Submit Query');
button.className = value.className || 'submitButtonProxy';
// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
Line 5,914 ⟶ 6,007:
if (this.hasFooterLinks) {
var bullet = document.createElement('span');
bullet.textContent = msg('bullet-separator', ' \u2022 '); // U+2022 BULLET
if (prep) {
$footerlinks.prepend(bullet);
|