الفرق بين الصفحتين: «ميدياويكي:Gadget-morebits.js» و«ويكيبيديا:لمح البصر/ورشة/سكريبتات/Gadget-morebits.js»
(الفرق بين الصفحتين)
تم حذف المحتوى تمت إضافة المحتوى
Maintenance: mw:RL/MGU - Updated deprecated module name |
ط بوت: تعريب |
||
سطر 1: | سطر 1: | ||
== الجزء الأول == |
|||
// Source: https://en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-morebits.js&oldid=993216096 |
|||
<syntaxhighlight lang="js" line="1"> |
|||
// <nowiki> |
// <nowiki> |
||
/** |
/** |
||
* morebits.js |
|||
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia. |
|||
* =========== |
|||
* |
|||
* مكتبة فيها الكثير من الشيفرات لويكيات ميديا ويكي بما فيها ويكيبيديا. |
|||
* The highlights include: |
|||
* |
|||
* - {@link Morebits.wiki.api} - make calls to the MediaWiki API |
|||
* الخطوط العامة لما تتضمنه المكتبة: |
|||
* - {@link Morebits.wiki.page} - modify pages on the wiki (edit, revert, delete, etc.) |
|||
* - |
* - Morebits.quickForm صنف - generates quick HTML forms on the fly |
||
* - Morebits.wiki.api صنف - يقوم بالاستدعاءات لواجهة برمجية التطبيقات الخاصة بويكي. |
|||
* - {@link Morebits.quickForm} - generate quick HTML forms on the fly |
|||
* - Morebits.wiki.page صنف - يُعدِّل الصفحات في الويكي (تحرير أو استعادة أو حذف ... إلخ). |
|||
* - {@link Morebits.simpleWindow} - a wrapper for jQuery UI Dialog with a custom look and extra features |
|||
* - Morebits.wikitext صنف - يتضمن بعد الأدوات للتعامل من نصوص ويكي. |
|||
* - {@link Morebits.status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes |
|||
* - Morebits.status صنف - a rough-and-ready status message displayer, used by the Morebits.wiki classes |
|||
* - {@link Morebits.wikitext} - utilities for dealing with wikitext |
|||
* |
* - Morebits.simpleWindow صنف - a wrapper for jQuery UI Dialog with a custom look and extra features |
||
* - {@link Morebits.array} - utilities for manipulating arrays |
|||
* |
* |
||
* Dependencies: |
* Dependencies: |
||
* - The whole thing relies on jQuery. But most wikis should provide this by default. |
* - The whole thing relies on jQuery. But most wikis should provide this by default. |
||
* - |
* - Morebits.quickForm, Morebits.simpleWindow, and Morebits.status rely on the "morebits.css" file for their styling. |
||
* - |
* - Morebits.simpleWindow relies on jquery UI Dialog (from ResourceLoader module name 'jquery.ui'). |
||
* - Morebits.quickForm tooltips rely on Tipsy (ResourceLoader module name 'jquery.tipsy'). |
|||
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition: |
|||
* For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy]. |
|||
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title,jquery.ui]|morebits.js|morebits.css|GadgetName.js` |
|||
* - |
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition: |
||
* |
* * GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,jquery.ui,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js |
||
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition: |
|||
* and then load ext.gadget.morebits as one of the dependencies for the new gadget. |
|||
* * morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,jquery.ui,jquery.tipsy|hidden]|morebits.js|morebits.css |
|||
* and then load ext.gadget.morebits as one of the dependencies for the new gadget |
|||
* |
* |
||
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support. |
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support. |
||
* |
* |
||
* 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 |
* For queries, suggestions, help, etc., head to [[Wikipedia talk:Twinkle]] on English Wikipedia [http://en.wikipedia.org]. |
||
* The latest development source is available at |
* The latest development source is available at [https://github.com/azatoth/twinkle/blob/master/morebits.js]. |
||
* |
|||
* @namespace Morebits |
|||
*/ |
*/ |
||
سطر 37: | سطر 37: | ||
(function (window, document, $) { // Wrap entire file with anonymous function |
(function (window, document, $) { // Wrap entire file with anonymous function |
||
/** @lends Morebits */ |
|||
var Morebits = {}; |
var Morebits = {}; |
||
window.Morebits = Morebits; // allow global access |
window.Morebits = Morebits; // allow global access |
||
/** |
/** |
||
* **************** Morebits.userIsInGroup() **************** |
|||
* Simple helper function to see what groups a user might belong. |
|||
* Simple helper function to see what groups a user might belong |
|||
* |
|||
* @param {string} group |
* @param {string} group eg. `sysop`, `extendedconfirmed`, etc |
||
* @returns {boolean} |
* @returns {boolean} |
||
*/ |
*/ |
||
سطر 51: | سطر 51: | ||
return mw.config.get('wgUserGroups').indexOf(group) !== -1; |
return mw.config.get('wgUserGroups').indexOf(group) !== -1; |
||
}; |
}; |
||
// Used a lot |
|||
/** Hardcodes whether the user is a sysop, used a lot. |
|||
* |
|||
* @constant |
|||
* @type {boolean} |
|||
*/ |
|||
Morebits.userIsSysop = Morebits.userIsInGroup('sysop'); |
Morebits.userIsSysop = Morebits.userIsInGroup('sysop'); |
||
/** |
|||
* Utilities to help process IP addresses. |
|||
* |
|||
* @namespace Morebits.ip |
|||
* @memberof Morebits |
|||
*/ |
|||
Morebits.ip = { |
|||
/** |
|||
* 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()`} |
|||
* function from the IPUtils library. Adddresses are verbose, uppercase, |
|||
* normalized, and expanded to 8 words. |
|||
* |
|||
* @param {string} address - The IPv6 address, with or without CIDR. |
|||
* @returns {string} |
|||
*/ |
|||
sanitizeIPv6: function (address) { |
|||
address = address.trim(); |
|||
if (address === '') { |
|||
return null; |
|||
} |
|||
if (!mw.util.isIPv6Address(address, true)) { |
|||
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 |
|||
return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2'); |
|||
}, |
|||
/** |
|||
* Determine if the given IP address is a range. Just conjoins |
|||
* `mw.util.isIPAddress` with and without the `allowBlock` option. |
|||
* |
|||
* @param {string} ip |
|||
* @returns {boolean} - True if given a valid IP address range, false otherwise. |
|||
*/ |
|||
isRange: function (ip) { |
|||
return mw.util.isIPAddress(ip, true) && !mw.util.isIPAddress(ip); |
|||
}, |
|||
/** |
|||
* 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 |
|||
* for IPv4 and /32 for IPv6. |
|||
* |
|||
* @returns {boolean} - True for valid ranges within the CIDR limits, |
|||
* otherwise false (ranges outside the limit, single IPs, non-IPs). |
|||
*/ |
|||
validCIDR: function (ip) { |
|||
if (Morebits.ip.isRange(ip)) { |
|||
var subnet = parseInt(ip.match(/\/(\d{1,3})$/)[1], 10); |
|||
if (subnet) { // Should be redundant |
|||
if (mw.util.isIPv6Address(ip, true)) { |
|||
if (subnet >= 32) { |
|||
return true; |
|||
} |
|||
} else { |
|||
if (subnet >= 16) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
}, |
|||
/** |
|||
* Get the /64 subnet for an IPv6 address. |
|||
* |
|||
* @param {string} ipv6 - The IPv6 address, with or without a subnet. |
|||
* @returns {boolean|string} - False if not IPv6 or bigger than a 64, |
|||
* otherwise the (sanitized) /64 address. |
|||
*/ |
|||
get64: function (ipv6) { |
|||
if (!ipv6 || !mw.util.isIPv6Address(ipv6, true)) { |
|||
return false; |
|||
} |
|||
var subnetMatch = ipv6.match(/\/(\d{1,3})$/); |
|||
if (subnetMatch && parseInt(subnetMatch[1], 10) < 64) { |
|||
return false; |
|||
} |
|||
ipv6 = Morebits.ip.sanitizeIPv6(ipv6); |
|||
var ip_re = /^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/; |
|||
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64'); |
|||
} |
|||
}; |
|||
/** |
/** |
||
* **************** Morebits.sanitizeIPv6() **************** |
|||
* JavaScript translation of the MediaWiki core function IP::sanitizeIP() in |
|||
* includes/utils/IP.php. |
|||
* 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. |
||
* @param {string} address - The IPv6 address |
|||
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/libs/IPUtils/+/refs/heads/master/src/IPUtils.php#214|`IP::sanitizeIP()`} |
|||
* function from the IPUtils library. |
|||
* |
|||
* @param {string} address - The IPv6 address. |
|||
* @returns {string} |
* @returns {string} |
||
*/ |
*/ |
||
سطر 229: | سطر 107: | ||
} |
} |
||
// Remove leading zeros from each bloc as needed |
// Remove leading zeros from each bloc as needed |
||
address = address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2'); |
|||
}; |
|||
return address; |
|||
/** |
|||
* Determines whether the current page is a redirect or soft redirect. Fails |
|||
* to detect soft redirects on edit, history, etc. pages. Will attempt to |
|||
* detect Module:RfD, with the same failure points. |
|||
* |
|||
* @returns {boolean} |
|||
*/ |
|||
Morebits.isPageRedirect = function() { |
|||
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length); |
|||
}; |
}; |
||
/** |
|||
* Stores a normalized (underscores converted to spaces) version of the |
|||
* `wgPageName` variable. |
|||
* |
|||
* @type {string} |
|||
*/ |
|||
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' '); |
|||
/** |
/** |
||
* **************** Morebits.quickForm **************** |
|||
* Create a string for use in regex matching a page name, regardless of the |
|||
* Morebits.quickForm is a class for creation of simple and standard forms without much |
|||
* leading character's capitalization. |
|||
* specific coding. |
|||
* |
* |
||
* Index to Morebits.quickForm element types: |
|||
* @param {string} pageName - Page name without namespace. |
|||
* |
|||
* @returns {string} - For a page name `Foo bar`, returns the string `[Ff]oo bar`. |
|||
* select A combo box (aka drop-down). |
|||
* - Attributes: name, label, multiple, size, list, event, disabled |
|||
* option An element for a combo box. |
|||
* - Attributes: value, label, selected, disabled |
|||
* optgroup A group of "option"s. |
|||
* - Attributes: label, list |
|||
* field A fieldset (aka group box). |
|||
* - Attributes: name, label, disabled |
|||
* checkbox A checkbox. Must use "list" parameter. |
|||
* - Attributes: name, list, event |
|||
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup |
|||
* radio A radio button. Must use "list" parameter. |
|||
* - Attributes: name, list, event |
|||
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup |
|||
* input A text box. |
|||
* - Attributes: name, label, value, size, disabled, required, readonly, maxlength, event |
|||
* dyninput A set of text boxes with "Remove" buttons and an "Add" button. |
|||
* - Attributes: name, label, min, max, sublabel, value, size, maxlength, event |
|||
* hidden An invisible form field. |
|||
* - Attributes: name, value |
|||
* header A level 5 header. |
|||
* - Attributes: label |
|||
* div A generic placeholder element or label. |
|||
* - Attributes: name, label |
|||
* submit A submit button. Morebits.simpleWindow moves these to the footer of the dialog. |
|||
* - Attributes: name, label, disabled |
|||
* button A generic button. |
|||
* - Attributes: name, label, disabled, event |
|||
* textarea A big, multi-line text box. |
|||
* - Attributes: name, label, value, cols, rows, disabled, required, readonly |
|||
* fragment A DocumentFragment object. |
|||
* - No attributes, and no global attributes except adminonly |
|||
* |
|||
* Global attributes: id, className, style, tooltip, extra, adminonly |
|||
*/ |
*/ |
||
Morebits.pageNameRegex = function(pageName) { |
|||
return '[' + pageName[0].toUpperCase() + pageName[0].toLowerCase() + ']' + pageName.slice(1); |
|||
}; |
|||
/* **************** Morebits.quickForm **************** */ |
|||
/** |
/** |
||
* @constructor |
|||
* Creation of simple and standard forms without much specific coding. |
|||
* @param {event} event - Function to execute when form is submitted |
|||
* |
|||
* @param {string} [eventType=submit] - Type of the event (default: submit) |
|||
* @namespace Morebits.quickForm |
|||
* @memberof Morebits |
|||
* @class |
|||
* @param {event} event - Function to execute when form is submitted. |
|||
* @param {string} [eventType=submit] - Type of the event. |
|||
*/ |
*/ |
||
Morebits.quickForm = function QuickForm(event, eventType) { |
Morebits.quickForm = function QuickForm(event, eventType) { |
||
سطر 279: | سطر 167: | ||
/** |
/** |
||
* Renders the HTML output of the quickForm |
* Renders the HTML output of the quickForm |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @returns {HTMLElement} |
* @returns {HTMLElement} |
||
*/ |
*/ |
||
سطر 291: | سطر 177: | ||
/** |
/** |
||
* Append element to the form |
* Append element to the form |
||
* @param {(Object|Morebits.quickForm.element)} data - a quickform element, or the object with which |
|||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(object|Morebits.quickForm.element)} data - A quickform element, or the object with which |
|||
* a quickform element is constructed. |
* a quickform element is constructed. |
||
* @returns {Morebits.quickForm.element} - |
* @returns {Morebits.quickForm.element} - same as what is passed to the function |
||
*/ |
*/ |
||
Morebits.quickForm.prototype.append = function QuickFormAppend(data) { |
Morebits.quickForm.prototype.append = function QuickFormAppend(data) { |
||
سطر 303: | سطر 187: | ||
/** |
/** |
||
* @constructor |
|||
* Create a new element for the the form. |
|||
* @param {Object} data - Object representing the quickform element. See class documentation |
|||
* |
|||
* comment for available types and attributes for each. |
|||
* Index to Morebits.quickForm.element types: |
|||
* - Global attributes: id, className, style, tooltip, extra, adminonly |
|||
* - `select`: A combo box (aka drop-down). |
|||
* - Attributes: name, label, multiple, size, list, event, disabled |
|||
* - `option`: An element for a combo box. |
|||
* - Attributes: value, label, selected, disabled |
|||
* - `optgroup`: A group of "option"s. |
|||
* - Attributes: label, list |
|||
* - `field`: A fieldset (aka group box). |
|||
* - Attributes: name, label, disabled |
|||
* - `checkbox`: A checkbox. Must use "list" parameter. |
|||
* - Attributes: name, list, event |
|||
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup |
|||
* - `radio`: A radio button. Must use "list" parameter. |
|||
* - Attributes: name, list, event |
|||
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup |
|||
* - `input`: A text box. |
|||
* - Attributes: name, label, value, size, disabled, required, readonly, maxlength, event |
|||
* - `dyninput`: A set of text boxes with "Remove" buttons and an "Add" button. |
|||
* - Attributes: name, label, min, max, sublabel, value, size, maxlength, event |
|||
* - `hidden`: An invisible form field. |
|||
* - Attributes: name, value |
|||
* - `header`: A level 5 header. |
|||
* - Attributes: label |
|||
* - `div`: A generic placeholder element or label. |
|||
* - Attributes: name, label |
|||
* - `submit`: A submit button. Morebits.simpleWindow moves these to the footer of the dialog. |
|||
* - Attributes: name, label, disabled |
|||
* - `button`: A generic button. |
|||
* - Attributes: name, label, disabled, event |
|||
* - `textarea`: A big, multi-line text box. |
|||
* - Attributes: name, label, value, cols, rows, disabled, required, readonly |
|||
* - `fragment`: A DocumentFragment object. |
|||
* - No attributes, and no global attributes except adminonly. |
|||
* |
|||
* @memberof Morebits.quickForm |
|||
* @class |
|||
* @param {object} data - Object representing the quickform element. Should |
|||
* specify one of the available types from the index above, as well as any |
|||
* relevant and available attributes. |
|||
* @example new Morebits.quickForm.element({ |
|||
* name: 'target', |
|||
* type: 'input', |
|||
* label: 'Your target:', |
|||
* tooltip: 'Enter your target. Required.', |
|||
* required: true |
|||
* }); |
|||
*/ |
*/ |
||
Morebits.quickForm.element = function QuickFormElement(data) { |
Morebits.quickForm.element = function QuickFormElement(data) { |
||
سطر 359: | سطر 197: | ||
}; |
}; |
||
/** |
|||
* @memberof Morebits.quickForm.element |
|||
* @type {number} |
|||
*/ |
|||
Morebits.quickForm.element.id = 0; |
Morebits.quickForm.element.id = 0; |
||
/** |
/** |
||
* Appends an element to current element |
* Appends an element to current element |
||
* @param {Morebits.quickForm.element} data A quickForm element or the object required to |
|||
* |
|||
* |
* create the quickForm element |
||
* @ |
* @returns {Morebits.quickForm.element} The same element passed in |
||
* create the quickForm element. |
|||
* @returns {Morebits.quickForm.element} The same element passed in. |
|||
*/ |
*/ |
||
Morebits.quickForm.element.prototype.append = function QuickFormElementAppend(data) { |
Morebits.quickForm.element.prototype.append = function QuickFormElementAppend(data) { |
||
سطر 385: | سطر 217: | ||
/** |
/** |
||
* Renders the HTML output for the quickForm element |
* Renders the HTML output for the quickForm element |
||
* without parameters: |
* This should be called without parameters: form.render() |
||
* |
|||
* @memberof Morebits.quickForm.element |
|||
* @returns {HTMLElement} |
* @returns {HTMLElement} |
||
*/ |
*/ |
||
سطر 401: | سطر 231: | ||
}; |
}; |
||
/** @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; |
||
سطر 527: | سطر 356: | ||
subnode.values = current.value; |
subnode.values = current.value; |
||
subnode.setAttribute('value', current.value); |
subnode.setAttribute('value', current.value); |
||
subnode.setAttribute('name', current.name || data.name); |
|||
subnode.setAttribute('type', data.type); |
subnode.setAttribute('type', data.type); |
||
subnode.setAttribute('id', cur_id); |
subnode.setAttribute('id', cur_id); |
||
subnode.setAttribute('name', current.name || data.name); |
|||
// If name is provided on the individual checkbox, add a data-single |
|||
// attribute which indicates it isn't part of a list of checkboxes with |
|||
// same name. Used in getInputData() |
|||
if (current.name) { |
|||
subnode.setAttribute('data-single', 'data-single'); |
|||
} |
|||
if (current.checked) { |
if (current.checked) { |
||
سطر 618: | سطر 440: | ||
} |
} |
||
} |
} |
||
} |
|||
if (data.shiftClickSupport && data.type === 'checkbox') { |
|||
Morebits.checkboxShiftClickSupport(Morebits.quickForm.getElements(node, data.name)); |
|||
} |
} |
||
break; |
break; |
||
سطر 874: | سطر 693: | ||
return [ node, childContainder ]; |
return [ node, childContainder ]; |
||
}; |
|||
Morebits.quickForm.element.autoNWSW = function() { |
|||
return $(this).offset().top > ($(document).scrollTop() + ($(window).height() / 2)) ? 'sw' : 'nw'; |
|||
}; |
}; |
||
/** |
/** |
||
* Create a |
* Create a jquery.tipsy-based tooltip. |
||
* @requires jquery.tipsy |
|||
* |
|||
* @param {HTMLElement} node - the HTML element beside which a tooltip is to be generated |
|||
* @memberof Morebits.quickForm.element |
|||
* @param {Object} data - tooltip-related configuration data |
|||
* @requires jquery.ui |
|||
* @param {HTMLElement} node - The HTML element beside which a tooltip is to be generated. |
|||
* @param {object} data - Tooltip-related configuration data. |
|||
*/ |
*/ |
||
Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) { |
Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) { |
||
$('<span/>', { |
|||
var tooltipButton = node.appendChild(document.createElement('span')); |
|||
'class': 'ui-icon ui-icon-info ui-icon-inline morebits-tooltip' |
|||
tooltipButton.className = 'morebits-tooltipButton'; |
|||
}).appendTo(node).tipsy({ |
|||
tooltipButton.title = data.tooltip; // Provides the content for jQuery UI |
|||
'fallback': data.tooltip, |
|||
tooltipButton.appendChild(document.createTextNode('?')); |
|||
'fade': true, |
|||
$(tooltipButton).tooltip({ |
|||
'gravity': data.type === 'input' || data.type === 'select' ? |
|||
position: { my: 'left top', at: 'center bottom', collision: 'flipfit' }, |
|||
Morebits.quickForm.element.autoNWSW : $.fn.tipsy.autoWE, |
|||
// Deprecated in UI 1.12, but MW stuck on 1.9.2 indefinitely; see #398 and T71386 |
|||
'html': true, |
|||
tooltipClass: 'morebits-ui-tooltip' |
|||
'delayOut': 250 |
|||
}); |
}); |
||
}; |
}; |
||
سطر 899: | سطر 721: | ||
// Some utility methods for manipulating quickForms after their creation: |
// Some utility methods for manipulating quickForms after their creation: |
||
// (None of these work for "dyninput" type fields at present) |
// (None of these work for "dyninput" type fields at present) |
||
/** |
|||
* Returns an object containing all filled form data entered by the user, with the object |
|||
* keys being the form element names. Disabled fields will be ignored, but not hidden fields. |
|||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {HTMLFormElement} form |
|||
* @returns {object} With field names as keys, input data as values. |
|||
*/ |
|||
Morebits.quickForm.getInputData = function(form) { |
|||
var result = {}; |
|||
for (var i = 0; i < form.elements.length; i++) { |
|||
var field = form.elements[i]; |
|||
if (field.disabled || !field.name || !field.type || |
|||
field.type === 'submit' || field.type === 'button') { |
|||
continue; |
|||
} |
|||
// For elements in subgroups, quickform prepends element names with |
|||
// name of the parent group followed by a period, get rid of that. |
|||
var fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1); |
|||
switch (field.type) { |
|||
case 'radio': |
|||
if (field.checked) { |
|||
result[fieldNameNorm] = field.value; |
|||
} |
|||
break; |
|||
case 'checkbox': |
|||
if (field.dataset.single) { |
|||
result[fieldNameNorm] = field.checked; // boolean |
|||
} else { |
|||
result[fieldNameNorm] = result[fieldNameNorm] || []; |
|||
if (field.checked) { |
|||
result[fieldNameNorm].push(field.value); |
|||
} |
|||
} |
|||
break; |
|||
case 'select-multiple': |
|||
result[fieldNameNorm] = $(field).val(); // field.value doesn't work |
|||
break; |
|||
case 'text': // falls through |
|||
case 'textarea': |
|||
result[fieldNameNorm] = field.value.trim(); |
|||
break; |
|||
default: // could be select-one, date, number, email, etc |
|||
if (field.value) { |
|||
result[fieldNameNorm] = field.value; |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
return result; |
|||
}; |
|||
/** |
/** |
||
* Returns all form elements with a given field name or ID |
* Returns all form elements with a given field name or ID |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {HTMLFormElement} form |
* @param {HTMLFormElement} form |
||
* @param {string} fieldName - |
* @param {string} fieldName - the name or id of the fields |
||
* @returns {HTMLElement[]} - |
* @returns {HTMLElement[]} - array of matching form elements |
||
*/ |
*/ |
||
Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) { |
Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) { |
||
var $form = $(form); |
var $form = $(form); |
||
fieldName = $.escapeSelector(fieldName); // sanitize input |
|||
var $elements = $form.find('[name="' + fieldName + '"]'); |
var $elements = $form.find('[name="' + fieldName + '"]'); |
||
if ($elements.length > 0) { |
if ($elements.length > 0) { |
||
سطر 972: | سطر 736: | ||
} |
} |
||
$elements = $form.find('#' + fieldName); |
$elements = $form.find('#' + fieldName); |
||
if ($elements.length > 0) { |
|||
return $elements.toArray(); |
|||
} |
|||
return null; |
|||
}; |
}; |
||
سطر 978: | سطر 745: | ||
* Searches the array of elements for a checkbox or radio button with a certain |
* Searches the array of elements for a checkbox or radio button with a certain |
||
* `value` attribute, and returns the first such element. Returns null if not found. |
* `value` attribute, and returns the first such element. Returns null if not found. |
||
* @param {HTMLInputElement[]} elementArray - array of checkbox or radio elements |
|||
* |
|||
* @param {string} value - value to search for |
|||
* @memberof Morebits.quickForm |
|||
* @param {HTMLInputElement[]} elementArray - Array of checkbox or radio elements. |
|||
* @param {string} value - Value to search for. |
|||
* @returns {HTMLInputElement} |
* @returns {HTMLInputElement} |
||
*/ |
*/ |
||
سطر 995: | سطر 760: | ||
/** |
/** |
||
* Returns the |
* Returns the <div> containing the form element, or the form element itself |
||
* May not work as expected on checkboxes or radios |
* May not work as expected on checkboxes or radios |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {HTMLElement} element |
* @param {HTMLElement} element |
||
* @returns {HTMLElement} |
* @returns {HTMLElement} |
||
سطر 1٬015: | سطر 778: | ||
/** |
/** |
||
* Gets the HTML element that contains the label of the given form element |
* Gets the HTML element that contains the label of the given form element |
||
* (mainly for internal use) |
* (mainly for internal use) |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|Morebits.quickForm.element)} element |
* @param {(HTMLElement|Morebits.quickForm.element)} element |
||
* @returns {HTMLElement} |
* @returns {HTMLElement} |
||
سطر 1٬038: | سطر 799: | ||
/** |
/** |
||
* Gets the label text of the element |
* Gets the label text of the element |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|Morebits.quickForm.element)} element |
* @param {(HTMLElement|Morebits.quickForm.element)} element |
||
* @returns {string} |
* @returns {string} |
||
سطر 1٬054: | سطر 813: | ||
/** |
/** |
||
* Sets the label of the element to the given text |
* Sets the label of the element to the given text |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|Morebits.quickForm.element)} element |
* @param {(HTMLElement|Morebits.quickForm.element)} element |
||
* @param {string} labelText |
* @param {string} labelText |
||
* @returns {boolean} |
* @returns {boolean} true if succeeded, false if the label element is unavailable |
||
*/ |
*/ |
||
Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) { |
Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) { |
||
سطر 1٬072: | سطر 829: | ||
/** |
/** |
||
* Stores the element's current label, and temporarily sets the label to the given text |
* Stores the element's current label, and temporarily sets the label to the given text |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|Morebits.quickForm.element)} element |
* @param {(HTMLElement|Morebits.quickForm.element)} element |
||
* @param {string} temporaryLabelText |
* @param {string} temporaryLabelText |
||
* @returns {boolean} |
* @returns {boolean} true if succeeded, false if the label element is unavailable |
||
*/ |
*/ |
||
Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) { |
Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) { |
||
سطر 1٬087: | سطر 842: | ||
/** |
/** |
||
* Restores the label stored by overrideElementLabel |
* Restores the label stored by overrideElementLabel |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|Morebits.quickForm.element)} element |
* @param {(HTMLElement|Morebits.quickForm.element)} element |
||
* @returns {boolean} |
* @returns {boolean} true if succeeded, false if the label element is unavailable |
||
*/ |
*/ |
||
Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) { |
Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) { |
||
سطر 1٬101: | سطر 854: | ||
/** |
/** |
||
* Shows or hides a form element plus its label and tooltip |
* Shows or hides a form element plus its label and tooltip |
||
* @param {(HTMLElement|jQuery|string)} element HTML/jQuery element, or jQuery selector string |
|||
* |
|||
* @param {boolean} [visibility] Skip this to toggle visibility |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|jQuery|string)} element - HTML/jQuery element, or jQuery selector string. |
|||
* @param {boolean} [visibility] - Skip this to toggle visibility. |
|||
*/ |
*/ |
||
Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) { |
Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) { |
||
سطر 1٬112: | سطر 863: | ||
/** |
/** |
||
* Shows or hides the question mark icon (which displays the tooltip) next to a form element |
* Shows or hides the "question mark" icon (which displays the tooltip) next to a form element |
||
* |
|||
* @memberof Morebits.quickForm |
|||
* @param {(HTMLElement|jQuery)} element |
* @param {(HTMLElement|jQuery)} element |
||
* @param {boolean} [visibility] |
* @param {boolean} [visibility] Skip this to toggle visibility |
||
*/ |
*/ |
||
Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) { |
Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) { |
||
$(Morebits.quickForm.getElementContainer(element)).find('.morebits- |
$(Morebits.quickForm.getElementContainer(element)).find('.morebits-tooltip').toggle(visibility); |
||
}; |
}; |
||
سطر 1٬125: | سطر 874: | ||
/** |
/** |
||
* |
* **************** HTMLFormElement **************** |
||
*/ |
*/ |
||
/** |
/** |
||
* Returns an array containing the values of elements with the given name, that has it's |
|||
* Get checked items in the form. |
|||
* checked property set to true. (i.e. a checkbox or a radiobutton is checked), or select |
|||
* options that have selected set to true. (don't try to mix selects with radio/checkboxes, |
|||
* please) |
|||
* Type is optional and can specify if either radio or checkbox (for the event |
|||
* that both checkboxes and radiobuttons have the same name. |
|||
* |
* |
||
* XXX: Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 |
|||
* @function external:HTMLFormElement.getChecked |
|||
* in twinkleunlink.js, which is better |
|||
* @param {string} name - Find checked property of elements (i.e. a checkbox |
|||
* or a radiobutton) with the given name, or select options that have selected |
|||
* set to true (don't try to mix selects with radio/checkboxes). |
|||
* @param {string} [type] - Optionally specify either radio or checkbox (for |
|||
* the event that both checkboxes and radiobuttons have the same name). |
|||
* @returns {string[]} - Contains the values of elements with the given name |
|||
* checked property set to true. |
|||
*/ |
*/ |
||
HTMLFormElement.prototype.getChecked = function(name, type) { |
HTMLFormElement.prototype.getChecked = function(name, type) { |
||
var elements = this.elements[name]; |
var elements = this.elements[name]; |
||
if (!elements) { |
if (!elements) { |
||
// if the element doesn't exists, return null. |
|||
return []; |
|||
return null; |
|||
} |
} |
||
var return_array = []; |
var return_array = []; |
||
سطر 1٬182: | سطر 932: | ||
/** |
/** |
||
* getUnchecked: |
|||
* Does the same as {@link HTMLFormElement.getChecked|getChecked}, but with unchecked elements. |
|||
* Does the same as getChecked above, but with unchecked elements. |
|||
* |
|||
* @function external:HTMLFormElement.getUnchecked |
|||
* @param {string} name - Find checked property of elements (i.e. a checkbox |
|||
* or a radiobutton) with the given name, or select options that have selected |
|||
* set to true (don't try to mix selects with radio/checkboxes). |
|||
* @param {string} [type] - Optionally specify either radio or checkbox (for |
|||
* the event that both checkboxes and radiobuttons have the same name). |
|||
* @returns {string[]} - Contains the values of elements with the given name |
|||
* checked property set to true. |
|||
*/ |
*/ |
||
HTMLFormElement.prototype.getUnchecked = function(name, type) { |
HTMLFormElement.prototype.getUnchecked = function(name, type) { |
||
var elements = this.elements[name]; |
var elements = this.elements[name]; |
||
if (!elements) { |
if (!elements) { |
||
// if the element doesn't exists, return null. |
|||
return []; |
|||
return null; |
|||
} |
} |
||
var return_array = []; |
var return_array = []; |
||
سطر 1٬237: | سطر 981: | ||
/** |
/** |
||
* |
* **************** RegExp **************** |
||
*/ |
|||
/** |
|||
* Deprecated as of September 2020, use {@link Morebits.string.escapeRegExp} |
|||
* or `mw.util.escapeRegExp`. |
|||
* |
* |
||
* Escapes a string to be used in a RegExp |
|||
* @function external:RegExp.escape |
|||
* @param {string} text - string to be escaped |
|||
* @deprecated Use {@link Morebits.string.escapeRegExp} or `mw.util.escapeRegExp`. |
|||
* @param {boolean} [space_fix=false] - Set true to replace spaces and underscores with `[ _]` as they are |
|||
* @param {string} text - String to be escaped. |
|||
* often equivalent |
|||
* @param {boolean} [space_fix=false] - Whether to replace spaces and |
|||
* @returns {string} - the escaped text |
|||
* underscores with `[ _]` as they are often equivalent. |
|||
* @returns {string} - The escaped text. |
|||
*/ |
*/ |
||
RegExp.escape = function(text, space_fix) { |
RegExp.escape = function(text, space_fix) { |
||
text = mw.util.escapeRegExp(text); |
|||
// Special MediaWiki escape - underscore/space are often equivalent |
|||
if (space_fix) { |
if (space_fix) { |
||
text = text.replace(/ |_/g, '[_ ]'); |
|||
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 |
return text; |
||
}; |
}; |
||
/** |
/** |
||
* **************** String; Morebits.string **************** |
|||
* Helper functions to manipulate strings. |
|||
* |
|||
* @namespace Morebits.string |
|||
* @memberof Morebits |
|||
*/ |
*/ |
||
Morebits.string = { |
Morebits.string = { |
||
// Helper functions to change case of a string |
|||
/** |
|||
* @param {string} str |
|||
* @returns {string} |
|||
*/ |
|||
toUpperCaseFirstChar: function(str) { |
toUpperCaseFirstChar: function(str) { |
||
str = str.toString(); |
str = str.toString(); |
||
return str.substr(0, 1).toUpperCase() + str.substr(1); |
return str.substr(0, 1).toUpperCase() + str.substr(1); |
||
}, |
}, |
||
/** |
|||
* @param {string} str |
|||
* @returns {string} |
|||
*/ |
|||
toLowerCaseFirstChar: function(str) { |
toLowerCaseFirstChar: function(str) { |
||
str = str.toString(); |
str = str.toString(); |
||
سطر 1٬285: | سطر 1٬017: | ||
/** |
/** |
||
* Gives an array of substrings of `str` |
* Gives an array of substrings of `str` starting with `start` and |
||
* ending with `end` |
* ending with `end`, which is not in `skiplist` |
||
* on wikitext with templates or links. |
|||
* |
|||
* @param {string} str |
* @param {string} str |
||
* @param {string} start |
* @param {string} start |
||
* @param {string} end |
* @param {string} end |
||
* @param {(string[]|string)} [skiplist] |
* @param {(string[]|string)} [skiplist] |
||
* @returns { |
* @returns {String[]} |
||
* @throws If the `start` and `end` strings aren't of the same length. |
|||
* @throws If `skiplist` isn't an array or string |
|||
*/ |
*/ |
||
splitWeightedByKeys: function(str, start, end, skiplist) { |
splitWeightedByKeys: function(str, start, end, skiplist) { |
||
سطر 1٬340: | سطر 1٬068: | ||
/** |
/** |
||
* Formats freeform "reason" (from a textarea) for deletion/other |
* Formats freeform "reason" (from a textarea) for deletion/other templates |
||
* |
* that are going to be substituted, (e.g. PROD, XFD, RPP) |
||
* Handles `|` outside a nowiki tag. |
|||
* |
|||
* @param {string} str |
* @param {string} str |
||
* @returns {string} |
* @returns {string} |
||
سطر 1٬356: | سطر 1٬082: | ||
/** |
/** |
||
* Like `String.prototype.replace()`, but escapes any dollar signs in the replacement string. |
|||
* Formats a "reason" (from a textarea) for inclusion in a userspace |
|||
* Useful when the the replacement string is arbitrary, such as a username or freeform user input, |
|||
* log. Replaces newlines with {{Pb}}, and adds an extra `#` before |
|||
* |
* and could contain dollar signs. |
||
* @param {string} string - text in which to replace |
|||
* |
|||
* @param {string} str |
|||
* @returns {string} |
|||
*/ |
|||
formatReasonForLog: function(str) { |
|||
return str |
|||
// handle line breaks, which otherwise break numbering |
|||
.replace(/\n+/g, '{{pb}}') |
|||
// put an extra # in front before bulleted or numbered list items |
|||
.replace(/^(#+)/mg, '#$1') |
|||
.replace(/^(\*+)/mg, '#$1'); |
|||
}, |
|||
/** |
|||
* Like `String.prototype.replace()`, but escapes any dollar signs in |
|||
* the replacement string. Useful when the the replacement string is |
|||
* arbitrary, such as a username or freeform user input, and could |
|||
* contain dollar signs. |
|||
* |
|||
* @param {string} string - Text in which to replace. |
|||
* @param {(string|RegExp)} pattern |
* @param {(string|RegExp)} pattern |
||
* @param {string} replacement |
* @param {string} replacement |
||
سطر 1٬385: | سطر 1٬092: | ||
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) { |
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) { |
||
return string.replace(pattern, replacement.replace(/\$/g, '$$$$')); |
return string.replace(pattern, replacement.replace(/\$/g, '$$$$')); |
||
}, |
|||
/** |
|||
* Determine if the user-provided expiration will be considered an |
|||
* infinite-length by MW. |
|||
* |
|||
* @see {@link https://phabricator.wikimedia.org/T68646} |
|||
* |
|||
* @param {string} expiry |
|||
* @returns {boolean} |
|||
*/ |
|||
isInfinity: function morebitsStringIsInfinity(expiry) { |
|||
return ['indefinite', 'infinity', 'infinite', 'never'].indexOf(expiry) !== -1; |
|||
}, |
|||
/** |
|||
* Escapes a string to be used in a RegExp, replacing spaces and |
|||
* underscores with `[ _]` as they are often equivalent. |
|||
* Replaced RegExp.escape September 2020. |
|||
* |
|||
* @param {string} text - String to be escaped. |
|||
* @returns {string} - The escaped text. |
|||
*/ |
|||
escapeRegExp: function(text) { |
|||
return mw.util.escapeRegExp(text).replace(/ |_/g, '[_ ]'); |
|||
} |
} |
||
}; |
}; |
||
سطر 1٬415: | سطر 1٬097: | ||
/** |
/** |
||
* **************** Morebits.array **************** |
|||
* Helper functions to manipulate arrays. |
|||
* |
|||
* @namespace Morebits.array |
|||
* @memberof Morebits |
|||
*/ |
*/ |
||
Morebits.array = { |
Morebits.array = { |
||
/** |
/** |
||
* @returns {Array} a copy of the array with duplicates removed |
|||
* Remove duplicated items from an array. |
|||
* |
|||
* @param {Array} arr |
|||
* @returns {Array} A copy of the array with duplicates removed. |
|||
* @throws When provided a non-array. |
|||
*/ |
*/ |
||
uniq: function(arr) { |
uniq: function(arr) { |
||
سطر 1٬432: | سطر 1٬108: | ||
throw 'A non-array object passed to Morebits.array.uniq'; |
throw 'A non-array object passed to Morebits.array.uniq'; |
||
} |
} |
||
var result = []; |
|||
return arr.filter(function(item, idx) { |
|||
for (var i = 0; i < arr.length; ++i) { |
|||
return arr.indexOf(item) === idx; |
|||
var current = arr[i]; |
|||
}); |
|||
if (result.indexOf(current) === -1) { |
|||
result.push(current); |
|||
} |
|||
} |
|||
return result; |
|||
}, |
}, |
||
/** |
/** |
||
* @returns {Array} a copy of the array with the first instance of each value |
|||
* Remove non-duplicated items from an array. |
|||
* removed; subsequent instances of those values (duplicates) remain |
|||
* |
|||
* @param {Array} arr |
|||
* @returns {Array} A copy of the array with the first instance of each value |
|||
* removed; subsequent instances of those values (duplicates) remain. |
|||
* @throws When provided a non-array. |
|||
*/ |
*/ |
||
dups: function(arr) { |
dups: function(arr) { |
||
سطر 1٬449: | سطر 1٬126: | ||
throw 'A non-array object passed to Morebits.array.dups'; |
throw 'A non-array object passed to Morebits.array.dups'; |
||
} |
} |
||
var uniques = []; |
|||
return arr.filter(function(item, idx) { |
|||
var result = []; |
|||
return arr.indexOf(item) !== idx; |
|||
for (var i = 0; i < arr.length; ++i) { |
|||
}); |
|||
var current = arr[i]; |
|||
if (uniques.indexOf(current) === -1) { |
|||
uniques.push(current); |
|||
} else { |
|||
result.push(current); |
|||
} |
|||
} |
|||
return result; |
|||
}, |
}, |
||
سطر 1٬457: | سطر 1٬142: | ||
/** |
/** |
||
* Break up an array into smaller arrays. |
* Break up an array into smaller arrays. |
||
* |
|||
* @param {Array} arr |
* @param {Array} arr |
||
* @param {number} size - Size of each chunk (except the last, which could be different) |
* @param {number} size - Size of each chunk (except the last, which could be different) |
||
* @returns {Array |
* @returns {Array} an array of these smaller arrays |
||
* @throws When provided a non-array. |
|||
*/ |
*/ |
||
chunk: function(arr, size) { |
chunk: function(arr, size) { |
||
سطر 1٬470: | سطر 1٬153: | ||
return [ arr ]; // we return an array consisting of this array. |
return [ arr ]; // we return an array consisting of this array. |
||
} |
} |
||
var |
var result = []; |
||
var current; |
|||
var result = new Array(numChunks); |
|||
for (var i = 0; i < |
for (var i = 0; i < arr.length; ++i) { |
||
if (i % size === 0) { // when 'i' is 0, this is always true, so we start by creating one. |
|||
result[i] = arr.slice(i * size, (i + 1) * size); |
|||
current = []; |
|||
result.push(current); |
|||
} |
|||
current.push(arr[i]); |
|||
} |
} |
||
return result; |
return result; |
||
سطر 1٬480: | سطر 1٬167: | ||
/** |
/** |
||
* ************ Morebits.select2 *************** |
|||
* Utilities to enhance select2 menus. See twinklewarn, twinklexfd, |
|||
* Utilities to enhance select2 menus |
|||
* twinkleblock for sample usages. |
|||
* See twinklewarn, twinklexfd, twinkleblock for sample usages |
|||
* |
|||
* @see {@link https://select2.org/} |
|||
* |
|||
* @namespace Morebits.select2 |
|||
* @memberof Morebits |
|||
* @requires jquery.select2 |
|||
*/ |
*/ |
||
Morebits.select2 = { |
Morebits.select2 = { |
||
matchers: { |
matchers: { |
||
/** |
/** |
||
* Custom matcher in which if the optgroup name matches, all options in that |
* Custom matcher in which if the optgroup name matches, all options in that |
||
* group are shown, like in jquery.chosen |
* group are shown, like in jquery.chosen |
||
*/ |
*/ |
||
optgroupFull: function(params, data) { |
optgroupFull: function(params, data) { |
||
سطر 1٬506: | سطر 1٬189: | ||
}, |
}, |
||
/** Custom matcher that matches from the beginning of words only |
/** Custom matcher that matches from the beginning of words only */ |
||
wordBeginning: function(params, data) { |
wordBeginning: function(params, data) { |
||
var originalMatcher = $.fn.select2.defaults.defaults.matcher; |
var originalMatcher = $.fn.select2.defaults.defaults.matcher; |
||
سطر 1٬518: | سطر 1٬201: | ||
}, |
}, |
||
/** Underline matched part of options |
/** Underline matched part of options */ |
||
highlightSearchMatches: function(data) { |
highlightSearchMatches: function(data) { |
||
var searchTerm = Morebits.select2SearchQuery; |
var searchTerm = Morebits.select2SearchQuery; |
||
سطر 1٬536: | سطر 1٬219: | ||
}, |
}, |
||
/** Intercept query as it is happening, for use in highlightSearchMatches |
/** Intercept query as it is happening, for use in highlightSearchMatches */ |
||
queryInterceptor: function(params) { |
queryInterceptor: function(params) { |
||
Morebits.select2SearchQuery = params && params.term; |
Morebits.select2SearchQuery = params && params.term; |
||
سطر 1٬542: | سطر 1٬225: | ||
/** |
/** |
||
* Open dropdown and begin search when the |
* Open dropdown and begin search when the .select2-selection has focus and a key is pressed |
||
* https://github.com/select2/select2/issues/3279#issuecomment-442524147 |
|||
* focus and a key is pressed. |
|||
* |
|||
* @see {@link https://github.com/select2/select2/issues/3279#issuecomment-442524147} |
|||
*/ |
*/ |
||
autoStart: function(ev) { |
autoStart: function(ev) { |
||
سطر 1٬566: | سطر 1٬247: | ||
/** |
/** |
||
* **************** Morebits.pageNameNorm **************** |
|||
* Temporarily hide a part of a string while processing the rest of it. |
|||
* Stores a normalized version of the wgPageName variable (underscores converted to spaces). |
|||
* Used by {@link Morebits.wikitext.page#commentOutImage|Morebits.wikitext.page.commentOutImage}. |
|||
* For queen/king/whatever and country! |
|||
*/ |
|||
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' '); |
|||
/** |
|||
* *************** Morebits.pageNameRegex ***************** |
|||
* For a page name 'Foo bar', returns the string '[Ff]oo bar' |
|||
* @param {string} pageName - page name without namespace |
|||
* @returns {string} |
|||
*/ |
|||
Morebits.pageNameRegex = function(pageName) { |
|||
return '[' + pageName[0].toUpperCase() + pageName[0].toLowerCase() + ']' + pageName.slice(1); |
|||
}; |
|||
/** |
|||
* **************** Morebits.unbinder **************** |
|||
* Used for temporarily hiding a part of a string while processing the rest of it. |
|||
* |
* |
||
* eg. var u = new Morebits.unbinder("Hello world <!-- world --> world"); |
|||
* @memberof Morebits |
|||
* u.unbind('<!--','-->'); |
|||
* @class |
|||
* u.content = u.content.replace(/world/g, 'earth'); |
|||
* @param {string} string - The initial text to process. |
|||
* |
* u.rebind(); // gives "Hello earth <!-- world --> earth" |
||
* |
|||
* u.unbind('<!--', '-->'); // text inside comment remains intact |
|||
* Text within the 'unbinded' part (in this case, the HTML comment) remains intact |
|||
* u.content = u.content.replace(/world/g, 'earth'); |
|||
* unbind() can be called multiple times to unbind multiple parts of the string. |
|||
* u.rebind(); // gives 'Hello earth <!-- world --> earth' |
|||
* |
|||
* Used by Morebits.wikitext.page.commentOutImage |
|||
*/ |
|||
/** |
|||
* @constructor |
|||
* @param {string} string |
|||
*/ |
*/ |
||
Morebits.unbinder = function Unbinder(string) { |
Morebits.unbinder = function Unbinder(string) { |
||
سطر 1٬581: | سطر 1٬288: | ||
throw new Error('not a string'); |
throw new Error('not a string'); |
||
} |
} |
||
/** The text being processed. */ |
|||
this.content = string; |
this.content = string; |
||
this.counter = 0; |
this.counter = 0; |
||
سطر 1٬591: | سطر 1٬297: | ||
Morebits.unbinder.prototype = { |
Morebits.unbinder.prototype = { |
||
/** |
/** |
||
* Hide the region encapsulated by the `prefix` and `postfix` from |
|||
* string processing. `prefix` and `postfix` will be used in a |
|||
* RegExp, so items that need escaping should be use `\\`. |
|||
* |
|||
* @param {string} prefix |
* @param {string} prefix |
||
* @param {string} postfix |
* @param {string} postfix |
||
* @throws If either `prefix` or `postfix` is missing. |
|||
*/ |
*/ |
||
unbind: function UnbinderUnbind(prefix, postfix) { |
unbind: function UnbinderUnbind(prefix, postfix) { |
||
if (!prefix || !postfix) { |
|||
throw new Error('Both prefix and postfix must be provided'); |
|||
} |
|||
var re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g'); |
var re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g'); |
||
this.content = this.content.replace(re, Morebits.unbinder.getCallback(this)); |
this.content = this.content.replace(re, Morebits.unbinder.getCallback(this)); |
||
}, |
}, |
||
/** @returns {string} The output */ |
|||
/** |
|||
* Restore the hidden portion of the `content` string. |
|||
* |
|||
* @returns {string} The processed output. |
|||
*/ |
|||
rebind: function UnbinderRebind() { |
rebind: function UnbinderRebind() { |
||
var content = this.content; |
var content = this.content; |
||
سطر 1٬628: | سطر 1٬322: | ||
history: null // {} |
history: null // {} |
||
}; |
}; |
||
/** @memberof Morebits.unbinder */ |
|||
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { |
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { |
||
return function UnbinderCallback(match) { |
return function UnbinderCallback(match) { |
||
سطر 1٬639: | سطر 1٬333: | ||
/* **************** Morebits.date **************** */ |
|||
/** |
/** |
||
* **************** Date **************** |
|||
* Create a date object with enhanced processing capabilities, a la {@link |
|||
* Helper functions to get the month as a string instead of a number |
|||
* https://momentjs.com/|moment.js}. MediaWiki timestamp format is also |
|||
* acceptable, in addition to everything that JS Date() accepts. |
|||
* |
* |
||
* @ |
* @deprecated Since early 2020 in favor of Morebits.date (#814) |
||
* |
*/ |
||
Date.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', |
|||
'July', 'August', 'September', 'October', 'November', 'December' ]; |
|||
Date.monthNamesAbbrev = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
|||
Date.prototype.getUTCMonthName = function() { |
|||
console.warn("NOTE: Date prototypes from Twinkle's Morebits (such as getUTCMonthName) have been deprecated, use Morebits.date instead"); // eslint-disable-line no-console |
|||
return Date.monthNames[this.getUTCMonth()]; |
|||
}; |
|||
Date.prototype.getUTCMonthNameAbbrev = function() { |
|||
console.warn("NOTE: Date prototypes from Twinkle's Morebits (such as getUTCMonthNameAbbrev) have been deprecated, use Morebits.date instead"); // eslint-disable-line no-console |
|||
return Date.monthNamesAbbrev[this.getUTCMonth()]; |
|||
}; |
|||
/** |
|||
* **************** Morebits.date **************** |
|||
*/ |
|||
/** |
|||
* @constructor |
|||
* Create a date object. MediaWiki timestamp format is also acceptable, |
|||
* in addition to everything that JS Date() accepts. |
|||
*/ |
*/ |
||
Morebits.date = function() { |
Morebits.date = function() { |
||
var args = Array.prototype.slice.call(arguments); |
var args = Array.prototype.slice.call(arguments); |
||
if (typeof args[0] === 'string') { |
|||
// Check if it's a MediaWiki signature timestamp (which the native Date cannot parse directly) |
|||
// Attempt to remove a comma and paren-wrapped timezone, to get MediaWiki timestamps to parse |
|||
// Must be first since firefox erroneously accepts the format, sans timezone |
|||
// Firefox (at least in 75) seems to be okay with the comma, though |
|||
// See also: #921, #936, #1174, #1187 |
|||
args[0] = args[0].replace(/(\d\d:\d\d),/, '$1').replace(/\(UTC\)/, 'UTC'); |
|||
var dateParts; |
|||
if (args.length === 1 && typeof args[0] === 'string' && (dateParts = Morebits.date.localeData.signatureTimestampFormat(args[0]))) { |
|||
this._d = new Date(Date.UTC.apply(null, dateParts)); |
|||
} else { |
|||
// Try standard date |
|||
this._d = new (Function.prototype.bind.apply(Date, [Date].concat(args))); |
|||
} |
|||
// Still no? |
|||
if (!this.isValid()) { |
|||
mw.log.warn('Invalid Morebits.date initialisation:', args); |
|||
} |
} |
||
this._d = new (Function.prototype.bind.apply(Date, [Date].concat(args))); |
|||
}; |
}; |
||
/** |
|||
* Localized strings for date processing. |
|||
* |
|||
* @memberof Morebits.date |
|||
* @type {object.<string, string>} |
|||
* @property {string[]} months |
|||
* @property {string[]} monthsShort |
|||
* @property {string[]} days |
|||
* @property {string[]} daysShort |
|||
* @property {object.<string, string>} relativeTimes |
|||
* @private |
|||
*/ |
|||
Morebits.date.localeData = { |
Morebits.date.localeData = { |
||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], |
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], |
||
سطر 1٬693: | سطر 1٬385: | ||
pastWeek: '[Last] dddd [at] h:mm A', |
pastWeek: '[Last] dddd [at] h:mm A', |
||
other: 'YYYY-MM-DD' |
other: 'YYYY-MM-DD' |
||
}, |
|||
signatureTimestampFormat: function (str) { |
|||
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]]; |
|||
} |
} |
||
}; |
}; |
||
// Allow native Date.prototype methods to be used on Morebits.date objects |
|||
Morebits.date.prototype = { |
|||
Object.getOwnPropertyNames(Date.prototype).forEach(function(func) { |
|||
/** @returns {boolean} */ |
|||
Morebits.date.prototype[func] = function() { |
|||
return this._d[func].apply(this._d, Array.prototype.slice.call(arguments)); |
|||
}; |
|||
}); |
|||
$.extend(Morebits.date.prototype, { |
|||
isValid: function() { |
isValid: function() { |
||
return !isNaN(this.getTime()); |
return !isNaN(this.getTime()); |
||
}, |
}, |
||
/** @param {(Date|Morebits.date)} date */ |
|||
/** |
|||
* @param {(Date|Morebits.date)} date |
|||
* @returns {boolean} |
|||
*/ |
|||
isBefore: function(date) { |
isBefore: function(date) { |
||
return this.getTime() < date.getTime(); |
return this.getTime() < date.getTime(); |
||
}, |
}, |
||
/** |
|||
* @param {(Date|Morebits.date)} date |
|||
* @returns {boolean} |
|||
*/ |
|||
isAfter: function(date) { |
isAfter: function(date) { |
||
return this.getTime() > date.getTime(); |
return this.getTime() > date.getTime(); |
||
}, |
}, |
||
/** @ |
/** @return {string} */ |
||
getUTCMonthName: function() { |
getUTCMonthName: function() { |
||
return Morebits.date.localeData.months[this.getUTCMonth()]; |
return Morebits.date.localeData.months[this.getUTCMonth()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getUTCMonthNameAbbrev: function() { |
getUTCMonthNameAbbrev: function() { |
||
return Morebits.date.localeData.monthsShort[this.getUTCMonth()]; |
return Morebits.date.localeData.monthsShort[this.getUTCMonth()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getMonthName: function() { |
getMonthName: function() { |
||
return Morebits.date.localeData.months[this.getMonth()]; |
return Morebits.date.localeData.months[this.getMonth()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getMonthNameAbbrev: function() { |
getMonthNameAbbrev: function() { |
||
return Morebits.date.localeData.monthsShort[this.getMonth()]; |
return Morebits.date.localeData.monthsShort[this.getMonth()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getUTCDayName: function() { |
getUTCDayName: function() { |
||
return Morebits.date.localeData.days[this.getUTCDay()]; |
return Morebits.date.localeData.days[this.getUTCDay()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getUTCDayNameAbbrev: function() { |
getUTCDayNameAbbrev: function() { |
||
return Morebits.date.localeData.daysShort[this.getUTCDay()]; |
return Morebits.date.localeData.daysShort[this.getUTCDay()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getDayName: function() { |
getDayName: function() { |
||
return Morebits.date.localeData.days[this.getDay()]; |
return Morebits.date.localeData.days[this.getDay()]; |
||
}, |
}, |
||
/** @returns {string} */ |
|||
getDayNameAbbrev: function() { |
getDayNameAbbrev: function() { |
||
return Morebits.date.localeData.daysShort[this.getDay()]; |
return Morebits.date.localeData.daysShort[this.getDay()]; |
||
سطر 1٬766: | سطر 1٬438: | ||
* Add a given number of minutes, hours, days, months or years to the date. |
* Add a given number of minutes, hours, days, months or years to the date. |
||
* This is done in-place. The modified date object is also returned, allowing chaining. |
* This is done in-place. The modified date object is also returned, allowing chaining. |
||
* @param {number} number - should be an integer |
|||
* |
|||
* @param {number} number - Should be an integer. |
|||
* @param {string} unit |
* @param {string} unit |
||
* @throws |
* @throws {Error} if invalid or unsupported unit is given |
||
* @returns {Morebits.date} |
* @returns {Morebits.date} |
||
*/ |
*/ |
||
سطر 1٬793: | سطر 1٬464: | ||
* Subtracts a given number of minutes, hours, days, months or years to the date. |
* Subtracts a given number of minutes, hours, days, months or years to the date. |
||
* This is done in-place. The modified date object is also returned, allowing chaining. |
* This is done in-place. The modified date object is also returned, allowing chaining. |
||
* @param {number} number - should be an integer |
|||
* |
|||
* @param {number} number - Should be an integer. |
|||
* @param {string} unit |
* @param {string} unit |
||
* @throws |
* @throws {Error} if invalid or unsupported unit is given |
||
* @returns {Morebits.date} |
* @returns {Morebits.date} |
||
*/ |
*/ |
||
سطر 1٬805: | سطر 1٬475: | ||
/** |
/** |
||
* Formats the date into a string per the given format string. |
* Formats the date into a string per the given format string. |
||
* Replacement syntax is a subset of that in moment.js |
* Replacement syntax is a subset of that in moment.js. |
||
* @param {string} formatstr |
|||
* |
|||
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone), |
|||
* | Syntax | Output | |
|||
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC. |
|||
* |--------|--------| |
|||
* | H | Hours (24-hour) | |
|||
* | HH | Hours (24-hour, padded) | |
|||
* | h | Hours (12-hour) | |
|||
* | hh | Hours (12-hour, padded) | |
|||
* | A | AM or PM | |
|||
* | m | Minutes | |
|||
* | mm | Minutes (padded) | |
|||
* | s | Seconds | |
|||
* | ss | Seconds (padded) | |
|||
* | SSS | Milliseconds fragment, padded | |
|||
* | d | Day number of the week (Sun=0) | |
|||
* | ddd | Abbreviated day name | |
|||
* | dddd | Full day name | |
|||
* | D | Date | |
|||
* | DD | Date (padded) | |
|||
* | M | Month number (0-indexed) | |
|||
* | MM | Month number (0-indexed, padded) | |
|||
* | MMM | Abbreviated month name | |
|||
* | MMMM | Full month name | |
|||
* | Y | Year | |
|||
* | YY | Final two digits of year (20 for 2020, 42 for 1942) | |
|||
* | YYYY | Year (same as `Y`) | |
|||
* |
|||
* @param {string} formatstr - Format the date into a string, using |
|||
* the replacement syntax. Use `[` and `]` to escape items. If not |
|||
* provided, will return the ISO-8601-formatted string. |
|||
* @param {(string|number)} [zone=system] - `system` (for browser-default time zone), |
|||
* `utc`, or specify a time zone as number of minutes relative to UTC. |
|||
* @returns {string} |
* @returns {string} |
||
*/ |
*/ |
||
format: function(formatstr, zone) { |
format: function(formatstr, zone) { |
||
if (!this.isValid()) { |
|||
return 'Invalid date'; // Put the truth out, preferable to "NaNNaNNan NaN:NaN" or whatever |
|||
} |
|||
var udate = this; |
var udate = this; |
||
// create a new date object that will contain the date to display as system time |
// create a new date object that will contain the date to display as system time |
||
سطر 1٬852: | سطر 1٬491: | ||
} |
} |
||
var pad = function(num) { |
|||
// default to ISOString |
|||
return num < 10 ? '0' + num : num; |
|||
if (!formatstr) { |
|||
return udate.toISOString(); |
|||
} |
|||
var pad = function(num, len) { |
|||
len = len || 2; // Up to length of 00 + 1 |
|||
return ('00' + num).toString().slice(0 - len); |
|||
}; |
}; |
||
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds |
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(); |
||
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 ? 'PM' : 'AM'; |
||
سطر 1٬868: | سطر 1٬501: | ||
'mm': pad(m), 'm': m, |
'mm': pad(m), 'm': m, |
||
'ss': pad(s), 's': s, |
'ss': pad(s), 's': s, |
||
'SSS': pad(ms, 3), |
|||
'dddd': udate.getDayName(), 'ddd': udate.getDayNameAbbrev(), 'd': udate.getDay(), |
'dddd': udate.getDayName(), 'ddd': udate.getDayNameAbbrev(), 'd': udate.getDay(), |
||
'DD': pad(D), 'D': D, |
'DD': pad(D), 'D': D, |
||
سطر 1٬882: | سطر 1٬514: | ||
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y' |
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y' |
||
*/ |
*/ |
||
/H{1,2}|h{1,2}|m{1,2}|s{1,2} |
/H{1,2}|h{1,2}|m{1,2}|s{1,2}|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g, |
||
function(match) { |
function(match) { |
||
return replacementMap[match]; |
return replacementMap[match]; |
||
سطر 1٬892: | سطر 1٬524: | ||
/** |
/** |
||
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM". |
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM". |
||
* Similar to |
* Similar to calendar in moment.js, but with time zone support. |
||
* |
|||
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone), |
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone), |
||
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC |
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC |
||
* @returns {string} |
* @returns {string} |
||
*/ |
*/ |
||
سطر 1٬920: | سطر 1٬551: | ||
/** |
/** |
||
* |
* @returns {RegExp} that matches wikitext section titles such as ==December 2019== or |
||
* |
* === Jan 2018 === |
||
* |
|||
* @returns {RegExp} |
|||
*/ |
*/ |
||
monthHeaderRegex: function() { |
monthHeaderRegex: function() { |
||
سطر 1٬932: | سطر 1٬561: | ||
/** |
/** |
||
* Creates a wikitext section header with the month and year. |
* Creates a wikitext section header with the month and year. |
||
* @param {number} [level=2] - Header level (default 2) |
|||
* |
|||
* @param {number} [level=2] - Header level. Pass 0 for just the text |
|||
* with no wikitext markers (==). |
|||
* @returns {string} |
* @returns {string} |
||
*/ |
*/ |
||
monthHeader: function(level) { |
monthHeader: function(level) { |
||
level = level || 2; |
|||
// Default to 2, but allow for 0 or stringy numbers |
|||
level = parseInt(level, 10); |
|||
level = isNaN(level) ? 2 : level; |
|||
var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11 |
var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11 |
||
return header + ' ' + this.getUTCMonthName() + ' ' + this.getUTCFullYear() + ' ' + header; |
|||
if (header.length) { // wikitext-formatted header |
|||
return header + ' ' + text + ' ' + header; |
|||
} |
|||
return text; // Just the string |
|||
} |
} |
||
}; |
|||
// Allow native Date.prototype methods to be used on Morebits.date objects |
|||
Object.getOwnPropertyNames(Date.prototype).forEach(function(func) { |
|||
// Exclude methods that collide with PageTriage's Date.js external, which clobbers native Date: [[phab:T268513]] |
|||
if (['add', 'getDayName', 'getMonthName'].indexOf(func) === -1) { |
|||
Morebits.date.prototype[func] = function() { |
|||
return this._d[func].apply(this._d, Array.prototype.slice.call(arguments)); |
|||
}; |
|||
} |
|||
}); |
}); |
||
/* **************** Morebits.wiki **************** */ |
|||
/** |
/** |
||
* **************** Morebits.wiki **************** |
|||
* Various objects for wiki editing and API access, including {@link |
|||
* Various objects for wiki editing and API access |
|||
* Morebits.wiki.api} and {@link Morebits.wiki.page}. |
|||
* |
|||
* @namespace Morebits.wiki |
|||
* @memberof Morebits |
|||
*/ |
*/ |
||
Morebits.wiki = {}; |
Morebits.wiki = {}; |
||
/** |
/** |
||
* Determines whether the current page is a redirect or soft redirect |
|||
* @deprecated in favor of Morebits.isPageRedirect as of November 2020 |
|||
* (fails to detect soft redirects on edit, history, etc. pages) |
|||
* @memberof Morebits.wiki |
|||
* @returns {boolean} |
* @returns {boolean} |
||
*/ |
*/ |
||
Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() { |
Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() { |
||
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect')); |
|||
console.warn('NOTE: Morebits.wiki.isPageRedirect has been deprecated, use Morebits.isPageRedirect instead.'); // eslint-disable-line no-console |
|||
return Morebits.isPageRedirect(); |
|||
}; |
}; |
||
/* **************** Morebits.wiki.actionCompleted **************** */ |
|||
/** |
|||
* @memberof Morebits.wiki |
|||
* @type {number} |
|||
*/ |
|||
Morebits.wiki.numberOfActionsLeft = 0; |
|||
/** |
|||
* @memberof Morebits.wiki |
|||
* @type {number} |
|||
*/ |
|||
Morebits.wiki.nbrOfCheckpointsLeft = 0; |
|||
/** |
/** |
||
* **************** Morebits.wiki.actionCompleted **************** |
|||
* Display message and/or redirect to page upon completion of tasks. |
|||
* |
* |
||
* |
* Use of Morebits.wiki.actionCompleted(): |
||
* Every call to Morebits.wiki.api.post() results in the dispatch of |
|||
* asynchronous callback. Each callback can in turn make an additional call to |
|||
* an asynchronous callback. Each callback can in turn |
|||
* Morebits.wiki.api.post() to continue a processing sequence. At the |
|||
* make an additional call to Morebits.wiki.api.post() to continue a |
|||
* conclusion of the final callback of a processing sequence, it is not |
|||
* processing sequence. At the conclusion of the final callback |
|||
* possible to simply return to the original caller because there is no call |
|||
* of a processing sequence, it is not possible to simply return to the |
|||
* stack leading back to the original context. Instead, |
|||
* original caller because there is no call stack leading back to |
|||
* Morebits.wiki.actionCompleted.event() is called to display the result to |
|||
* the original context. Instead, Morebits.wiki.actionCompleted.event() is |
|||
* the user and to perform an optional page redirect. |
|||
* called to display the result to the user and to perform an optional |
|||
* page redirect. |
|||
* |
* |
||
* The determination of when to call Morebits.wiki.actionCompleted.event() |
* The determination of when to call Morebits.wiki.actionCompleted.event() |
||
* 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 |
||
* 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 |
||
* step in the processing chain and Morebits.wiki.actionCompleted.event() |
* final step in the processing chain and Morebits.wiki.actionCompleted.event() |
||
* then be called. |
* will then be called. |
||
* |
* |
||
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that |
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that |
||
* processing is not complete upon the conclusion of the final callback |
* 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 |
||
* |
* Morebits.wiki.removeCheckpoint(). |
||
* |
|||
* @memberof Morebits.wiki |
|||
*/ |
*/ |
||
Morebits.wiki.numberOfActionsLeft = 0; |
|||
Morebits.wiki.nbrOfCheckpointsLeft = 0; |
|||
Morebits.wiki.actionCompleted = function(self) { |
Morebits.wiki.actionCompleted = function(self) { |
||
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) { |
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) { |
||
سطر 2٬033: | سطر 1٬629: | ||
// Change per action wanted |
// Change per action wanted |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.actionCompleted.event = function() { |
Morebits.wiki.actionCompleted.event = function() { |
||
if (Morebits.wiki.actionCompleted.notice) { |
if (Morebits.wiki.actionCompleted.notice) { |
||
سطر 2٬052: | سطر 1٬647: | ||
}; |
}; |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut; |
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut; |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.actionCompleted.redirect = null; |
Morebits.wiki.actionCompleted.redirect = null; |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.actionCompleted.notice = null; |
Morebits.wiki.actionCompleted.notice = null; |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.addCheckpoint = function() { |
Morebits.wiki.addCheckpoint = function() { |
||
++Morebits.wiki.nbrOfCheckpointsLeft; |
++Morebits.wiki.nbrOfCheckpointsLeft; |
||
}; |
}; |
||
/** @memberof Morebits.wiki */ |
|||
Morebits.wiki.removeCheckpoint = function() { |
Morebits.wiki.removeCheckpoint = function() { |
||
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) { |
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) { |
||
سطر 2٬071: | سطر 1٬661: | ||
}; |
}; |
||
/** |
|||
* **************** Morebits.wiki.api **************** |
|||
* An easy way to talk to the MediaWiki API. |
|||
*/ |
|||
/* **************** Morebits.wiki.api **************** */ |
|||
/** |
/** |
||
* @constructor |
|||
* An easy way to talk to the MediaWiki API. Accepts either json or xml |
|||
* @param {string} currentAction - The current action (required) |
|||
* (default) formats; if json is selected, will default to `formatversion=2` |
|||
* @param {Object} query - The query (required) |
|||
* unless otherwise specified. Similarly, enforces newer `errorformat`s, |
|||
* @param {Function} [onSuccess] - The function to call when request gotten |
|||
* defaulting to `html` if unspecified. `uselang` enforced to the wiki's |
|||
* @param {Object} [statusElement] - A Morebits.status object to use for status messages (optional) |
|||
* content language. |
|||
* @param {Function} [onError] - The function to call if an error occurs (optional) |
|||
* |
|||
* In new code, the use of the last 3 parameters should be avoided, instead |
|||
* use {@link Morebits.wiki.api#setStatusElement|setStatusElement()} to bind |
|||
* the status element (if needed) and use `.then()` or `.catch()` on the |
|||
* promise returned by `post()`, rather than specify the `onSuccess` or |
|||
* `onFailure` callbacks. |
|||
* |
|||
* @memberof Morebits.wiki |
|||
* @class |
|||
* @param {string} currentAction - The current action (required). |
|||
* @param {object} query - The query (required). |
|||
* @param {Function} [onSuccess] - The function to call when request is successful. |
|||
* @param {Morebits.status} [statusElement] - A Morebits.status object to use for status messages. |
|||
* @param {Function} [onError] - The function to call if an error occurs. |
|||
*/ |
*/ |
||
Morebits.wiki.api = function(currentAction, query, onSuccess, statusElement, onError) { |
Morebits.wiki.api = function(currentAction, query, onSuccess, statusElement, onError) { |
||
سطر 2٬098: | سطر 1٬678: | ||
this.query = query; |
this.query = query; |
||
this.query.assert = 'user'; |
this.query.assert = 'user'; |
||
// Enforce newer error formats, preferring html |
|||
if (!query.errorformat || ['wikitext', 'plaintext'].indexOf(query.errorformat) === -1) { |
|||
this.query.errorformat = 'html'; |
|||
} |
|||
// Explicitly use the wiki's content language to minimize confusion, |
|||
// see #1179 for discussion |
|||
this.query.uselang = 'content'; |
|||
this.query.errorlang = 'uselang'; |
|||
this.query.errorsuselocal = 1; |
|||
this.onSuccess = onSuccess; |
this.onSuccess = onSuccess; |
||
this.onError = onError; |
this.onError = onError; |
||
if (statusElement) { |
if (statusElement) { |
||
this. |
this.statelem = statusElement; |
||
this.statelem.status(currentAction); |
|||
} else { |
} else { |
||
this.statelem = new Morebits.status(currentAction); |
this.statelem = new Morebits.status(currentAction); |
||
سطر 2٬117: | سطر 1٬688: | ||
if (!query.format) { |
if (!query.format) { |
||
this.query.format = 'xml'; |
this.query.format = 'xml'; |
||
} else if (query.format === 'json' && !query.formatversion) { |
|||
this.query.formatversion = '2'; |
|||
} else if (['xml', 'json'].indexOf(query.format) === -1) { |
} else if (['xml', 'json'].indexOf(query.format) === -1) { |
||
this.statelem.error('Invalid API format: only xml and json are supported.'); |
this.statelem.error('Invalid API format: only xml and json are supported.'); |
||
} |
|||
// Ignore tags for queries and most common unsupported actions, produces warnings |
|||
if (query.action && ['query', 'review', 'stabilize', 'pagetriageaction', 'watch'].indexOf(query.action) !== -1) { |
|||
delete query.tags; |
|||
} else if (!query.tags && morebitsWikiChangeTag) { |
|||
query.tags = morebitsWikiChangeTag; |
|||
} |
} |
||
}; |
}; |
||
سطر 2٬139: | سطر 1٬701: | ||
response: null, |
response: null, |
||
responseXML: null, // use `response` instead; retained for backwards compatibility |
responseXML: null, // use `response` instead; retained for backwards compatibility |
||
setParent: function(parent) { |
|||
this.parent = parent; |
|||
}, // keep track of parent object for callbacks |
|||
statelem: null, // this non-standard name kept for backwards compatibility |
statelem: null, // this non-standard name kept for backwards compatibility |
||
statusText: null, // result received from the API, normally "success" or "error" |
statusText: null, // result received from the API, normally "success" or "error" |
||
errorCode: null, // short text error code, if any, as documented in the MediaWiki API |
errorCode: null, // short text error code, if any, as documented in the MediaWiki API |
||
errorText: null, // full error description, if any |
errorText: null, // full error description, if any |
||
badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error |
|||
/** |
/** |
||
* Carries out the request. |
|||
* Keep track of parent object for callbacks. |
|||
* @param {Object} callerAjaxParameters Do not specify a parameter unless you really |
|||
* |
|||
* really want to give jQuery some extra parameters |
|||
* @param {*} parent |
|||
*/ |
|||
setParent: function(parent) { |
|||
this.parent = parent; |
|||
}, |
|||
/** @param {Morebits.status} statusElement */ |
|||
setStatusElement: function(statusElement) { |
|||
this.statelem = statusElement; |
|||
this.statelem.status(this.currentAction); |
|||
}, |
|||
/** |
|||
* Carry out the request. |
|||
* |
|||
* @param {object} callerAjaxParameters - Do not specify a parameter unless you really |
|||
* really want to give jQuery some extra parameters. |
|||
* @returns {promise} - A jQuery promise object that is resolved or rejected with the api object. |
|||
*/ |
*/ |
||
post: function(callerAjaxParameters) { |
post: function(callerAjaxParameters) { |
||
سطر 2٬182: | سطر 1٬729: | ||
var ajaxparams = $.extend({}, { |
var ajaxparams = $.extend({}, { |
||
context: this, |
context: this, |
||
type |
type: 'POST', |
||
url: mw.util.wikiScript('api'), |
url: mw.util.wikiScript('api'), |
||
data: queryString, |
data: queryString, |
||
dataType: |
dataType: 'xml', |
||
headers: { |
headers: { |
||
'Api-User-Agent': morebitsWikiApiUserAgent |
'Api-User-Agent': morebitsWikiApiUserAgent |
||
سطر 2٬191: | سطر 1٬738: | ||
}, callerAjaxParameters); |
}, callerAjaxParameters); |
||
return $.ajax(ajaxparams). |
return $.ajax(ajaxparams).done( |
||
function(response, statusText) { |
|||
function onAPIsuccess(response, statusText) { |
|||
this.statusText = statusText; |
this.statusText = statusText; |
||
this.response = this.responseXML = response; |
this.response = this.responseXML = response; |
||
// Limit to first error |
|||
if (this.query.format === 'json') { |
if (this.query.format === 'json') { |
||
this.errorCode = response. |
this.errorCode = response.error && response.error.code; |
||
this.errorText = response.error && response.error.info; |
|||
if (this.query.errorformat === 'html') { |
|||
this.errorText = response.errors && response.errors[0].html; |
|||
} else if (this.query.errorformat === 'wikitext' || this.query.errorformat === 'plaintext') { |
|||
this.errorText = response.errors && response.errors[0].text; |
|||
} |
|||
} else { |
} else { |
||
this.errorCode = $(response).find(' |
this.errorCode = $(response).find('error').attr('code'); |
||
this.errorText = $(response).find('error').attr('info'); |
|||
// Sufficient for html, wikitext, or plaintext errorformats |
|||
this.errorText = $(response).find('errors error').eq(0).text(); |
|||
} |
} |
||
if (typeof this.errorCode === 'string') { |
if (typeof this.errorCode === 'string') { |
||
// the API didn't like what we told it, e.g., bad edit token or an error creating a page |
// the API didn't like what we told it, e.g., bad edit token or an error creating a page |
||
this.returnError(); |
|||
return; |
|||
} |
} |
||
// invoke success callback if one was supplied |
// invoke success callback if one was supplied |
||
if (this.onSuccess) { |
if (this.onSuccess) { |
||
// set the callback context to this.parent for new code and supply the API object |
// set the callback context to this.parent for new code and supply the API object |
||
// as the first argument to the callback (for legacy code) |
// as the first argument to the callback (for legacy code) |
||
سطر 2٬225: | سطر 1٬768: | ||
Morebits.wiki.actionCompleted(); |
Morebits.wiki.actionCompleted(); |
||
} |
|||
).fail( |
|||
return $.Deferred().resolveWith(this.parent, [this]); |
|||
}, |
|||
// only network and server errors reach here - complaints from the API itself are caught in success() |
// only network and server errors reach here - complaints from the API itself are caught in success() |
||
function |
function(jqXHR, statusText, errorThrown) { |
||
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 = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.'; |
||
this.returnError(); |
|||
} |
} |
||
); // the return value should be ignored, unless using callerAjaxParameters with |async: false| |
|||
); |
|||
}, |
}, |
||
returnError: function( |
returnError: function() { |
||
if (this.errorCode === 'badtoken' |
if (this.errorCode === 'badtoken') { |
||
this.statelem. |
this.statelem.error('Invalid token. Refresh the page and try again'); |
||
} else { |
|||
this.badtokenRetry = true; |
|||
this.statelem.error(this.errorText); |
|||
// Get a new CSRF token and retry. If the original action needs a different |
|||
// type of action than CSRF, we do one pointless retry before bailing out |
|||
return Morebits.wiki.api.getToken().then(function(token) { |
|||
this.query.token = token; |
|||
return this.post(callerAjaxParameters); |
|||
}.bind(this)); |
|||
} |
} |
||
this.statelem.error(this.errorText + ' (' + this.errorCode + ')'); |
|||
// invoke failure callback if one was supplied |
// invoke failure callback if one was supplied |
||
سطر 2٬262: | سطر 1٬795: | ||
} |
} |
||
// don't complete the action so that the error remains displayed |
// don't complete the action so that the error remains displayed |
||
return $.Deferred().rejectWith(this.parent, [this]); |
|||
}, |
}, |
||
سطر 2٬288: | سطر 1٬819: | ||
}; |
}; |
||
// Custom user agent header, used by WMF for server-side logging |
|||
/** |
|||
// See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html |
|||
* Custom user agent header, used by WMF for server-side logging. Set via |
|||
var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])'; |
|||
* {@link Morebits.wiki.api.setApiUserAgent|setApiUserAgent}. |
|||
* |
|||
* @see {@link https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html} |
|||
* for original announcement. |
|||
* |
|||
* @memberof Morebits.wiki.api |
|||
* @type {string} |
|||
*/ |
|||
var morebitsWikiApiUserAgent = 'morebits.js ([[w:WT:TW]])'; |
|||
/** |
/** |
||
* Sets the custom user agent header |
* Sets the custom user agent header |
||
* @param {string} ua User agent |
|||
* |
|||
* @memberof Morebits.wiki.api |
|||
* @param {string} [ua] - User agent. |
|||
*/ |
*/ |
||
Morebits.wiki.api.setApiUserAgent = function(ua) { |
Morebits.wiki.api.setApiUserAgent = function(ua) { |
||
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js ([[w:WT:TW]])'; |
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js/2.0 ([[w:WT:TW]])'; |
||
}; |
}; |
||
سطر 2٬313: | سطر 1٬834: | ||
/** |
/** |
||
* **************** Morebits.wiki.page **************** |
|||
* Change/revision tag applied to Morebits actions when no other tags are specified. |
|||
* Uses the MediaWiki API to load a page and optionally edit it, move it, etc. |
|||
* Defaults to unused per {@link https://en.wikipedia.org/w/index.php?oldid=970618849#Adding_tags_to_Twinkle_edits_and_actions|consensus}. |
|||
* |
|||
* @constant |
|||
* @memberof Morebits.wiki.api |
|||
* @type {string} |
|||
*/ |
|||
var morebitsWikiChangeTag = ''; |
|||
/** |
|||
* Get a new CSRF token on encountering token errors. |
|||
* |
|||
* @memberof Morebits.wiki.api |
|||
* @returns {string} MediaWiki CSRF token. |
|||
*/ |
|||
Morebits.wiki.api.getToken = function() { |
|||
var tokenApi = new Morebits.wiki.api('Getting token', { |
|||
action: 'query', |
|||
meta: 'tokens', |
|||
type: 'csrf' |
|||
}); |
|||
return tokenApi.post().then(function(apiobj) { |
|||
return $(apiobj.responseXML).find('tokens').attr('csrftoken'); |
|||
}); |
|||
}; |
|||
/* **************** Morebits.wiki.page **************** */ |
|||
/** |
|||
* Use the MediaWiki API to load a page and optionally edit it, move it, etc. |
|||
* |
* |
||
* Callers are not permitted to directly access the properties of this class! |
* Callers are not permitted to directly access the properties of this class! |
||
* All property access is through the appropriate get___() or set___() method. |
* All property access is through the appropriate get___() or set___() method. |
||
* |
* |
||
* Callers should set |
* Callers should set Morebits.wiki.actionCompleted.notice and Morebits.wiki.actionCompleted.redirect |
||
* before the first call to |
* before the first call to Morebits.wiki.page.load(). |
||
* |
* |
||
* Each of the callback functions takes one parameter, which is a |
* Each of the callback functions takes one parameter, which is a |
||
سطر 2٬356: | سطر 1٬848: | ||
* |
* |
||
* |
* |
||
* HIGHLIGHTS: |
|||
* Call sequence for common operations (optional final user callbacks not shown): |
|||
* |
* |
||
* Constructor: Morebits.wiki.page(pageName, currentAction) |
|||
* - Edit current contents of a page (no edit conflict): |
|||
* pageName - the name of the page, prefixed by the namespace (if any) |
|||
* `.load(userTextEditCallback) -> ctx.loadApi.post() -> |
|||
* (for the current page, use mw.config.get('wgPageName')) |
|||
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() -> |
|||
* currentAction - a string describing the action about to be undertaken (optional) |
|||
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()` |
|||
* |
* |
||
* onSuccess and onFailure are callback functions called when the operation is a success or failure |
|||
* - Edit current contents of a page (with edit conflict): |
|||
* if enclosed in [brackets], it indicates that it is optional |
|||
* `.load(userTextEditCallback) -> ctx.loadApi.post() -> |
|||
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() -> |
|||
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnSaveError() -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> |
|||
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()` |
|||
* |
* |
||
* load(onSuccess, [onFailure]): Loads the text for the page |
|||
* - Append to a page (similar for prepend and newSection): |
|||
* `.append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> ctx.saveApi.post() -> |
|||
* ctx.loadApi.post.success() -> ctx.fnSaveSuccess()` |
|||
* |
* |
||
* getPageText(): returns a string containing the text of the page after a successful load() |
|||
* Notes: |
|||
* 1. All functions following Morebits.wiki.api.post() are invoked asynchronously from the jQuery AJAX library. |
|||
* 2. The sequence for append/prepend/newSection could be slightly shortened, |
|||
* but it would require significant duplication of code for little benefit. |
|||
* |
* |
||
* save([onSuccess], [onFailure]): Saves the text set via setPageText() for the page. |
|||
* Must be preceded by calling load(). |
|||
* Warning: Calling save() can result in additional calls to the previous load() callbacks to |
|||
* recover from edit conflicts! |
|||
* In this case, callers must make the same edit to the new pageText and reinvoke save(). |
|||
* This behavior can be disabled with setMaxConflictRetries(0). |
|||
* |
|||
* append([onSuccess], [onFailure]): Adds the text provided via setAppendText() to the end of |
|||
* the page. Does not require calling load() first. |
|||
* |
|||
* prepend([onSuccess], [onFailure]): Adds the text provided via setPrependText() to the start |
|||
* of the page. Does not require calling load() first. |
|||
* |
|||
* move([onSuccess], [onFailure]): Moves a page to another title |
|||
* |
|||
* patrol(): Patrols a page; ignores errors |
|||
* |
|||
* triage(): Marks page as reviewed using PageTriage, which implies patrolled; ignores errors |
|||
* |
|||
* deletePage([onSuccess], [onFailure]): Deletes a page (for admins only) |
|||
* |
|||
* undeletePage([onSuccess], [onFailure]): Undeletes a page (for admins only) |
|||
* |
|||
* protect([onSuccess], [onFailure]): Protects a page |
|||
* |
|||
* getPageName(): returns a string containing the name of the loaded page, including the namespace |
|||
* |
|||
* setPageText(pageText) sets the updated page text that will be saved when save() is called |
|||
* |
|||
* setAppendText(appendText) sets the text that will be appended to the page when append() is called |
|||
* |
|||
* setPrependText(prependText) sets the text that will be prepended to the page when prepend() is called |
|||
* |
|||
* setCallbackParameters(callbackParameters) |
|||
* callbackParameters - an object for use in a callback function |
|||
* |
|||
* getCallbackParameters(): returns the object previous set by setCallbackParameters() |
|||
* |
|||
* Callback notes: callbackParameters is for use by the caller only. The parameters |
|||
* allow a caller to pass the proper context into its callback function. |
|||
* Callers must ensure that any changes to the callbackParameters object |
|||
* within a load() callback still permit a proper re-entry into the |
|||
* load() callback if an edit conflict is detected upon calling save(). |
|||
* |
|||
* getStatusElement(): returns the Status element created by the constructor |
|||
* |
|||
* exists(): returns true if the page existed on the wiki when it was last loaded |
|||
* |
|||
* getCurrentID(): returns a string containing the current revision ID of the page |
|||
* |
|||
* lookupCreation(onSuccess): Retrieves the username and timestamp of page creation |
|||
* onSuccess - callback function which is called when the username and timestamp |
|||
* are found within the callback. |
|||
* The username can be retrieved using the getCreator() function; |
|||
* the timestamp can be retrieved using the getCreationTimestamp() function |
|||
* |
|||
* getCreator(): returns the user who created the page following lookupCreation() |
|||
* |
|||
* getCreationTimestamp(): returns an ISOString timestamp of page creation following lookupCreation() |
|||
* |
* |
||
*/ |
|||
* @memberof Morebits.wiki |
|||
* @class |
|||
/** |
|||
* @param {string} pageName - The name of the page, prefixed by the namespace (if any). |
|||
* Call sequence for common operations (optional final user callbacks not shown): |
|||
* For the current page, use `mw.config.get('wgPageName')`. |
|||
* |
|||
* @param {string} [currentAction] - A string describing the action about to be undertaken. |
|||
* Edit current contents of a page (no edit conflict): |
|||
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> |
|||
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() |
|||
* |
|||
* Edit current contents of a page (with edit conflict): |
|||
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> |
|||
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() -> |
|||
* ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> |
|||
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() |
|||
* |
|||
* Append to a page (similar for prepend): |
|||
* .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> |
|||
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> |
|||
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() |
|||
* |
|||
* Notes: |
|||
* 1. All functions following Morebits.wiki.api.post() are invoked asynchronously |
|||
* from the jQuery AJAX library. |
|||
* 2. The sequence for append/prepend could be slightly shortened, but it would require |
|||
* significant duplication of code for little benefit. |
|||
*/ |
|||
/** |
|||
* @constructor |
|||
* @param {string} pageName The name of the page, prefixed by the namespace (if any) |
|||
* For the current page, use mw.config.get('wgPageName') |
|||
* @param {string} [currentAction] A string describing the action about to be undertaken (optional) |
|||
*/ |
*/ |
||
Morebits.wiki.page = function(pageName, currentAction) { |
Morebits.wiki.page = function(pageName, currentAction) { |
||
سطر 2٬395: | سطر 1٬965: | ||
/** |
/** |
||
* Private context variables |
* Private context variables |
||
* |
* |
||
* This context is not visible to the outside, thus all the data here |
* This context is not visible to the outside, thus all the data here |
||
* must be accessed via getter and setter functions. |
* must be accessed via getter and setter functions. |
||
* |
|||
* @private |
|||
*/ |
*/ |
||
var ctx = { |
var ctx = { |
||
سطر 2٬407: | سطر 1٬975: | ||
pageExists: false, |
pageExists: false, |
||
editSummary: null, |
editSummary: null, |
||
changeTags: null, |
|||
testActions: null, // array if any valid actions |
|||
callbackParameters: null, |
callbackParameters: null, |
||
statusElement: new Morebits.status(currentAction), |
statusElement: new Morebits.status(currentAction), |
||
سطر 2٬417: | سطر 1٬983: | ||
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect |
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect |
||
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect |
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect |
||
newSectionText: null, |
|||
newSectionTitle: null, |
|||
createOption: null, |
createOption: null, |
||
minorEdit: false, |
minorEdit: false, |
||
سطر 2٬426: | سطر 1٬990: | ||
maxRetries: 2, |
maxRetries: 2, |
||
followRedirect: false, |
followRedirect: false, |
||
followCrossNsRedirect: true, |
|||
watchlistOption: 'nochange', |
watchlistOption: 'nochange', |
||
watchlistExpiry: null, |
|||
creator: null, |
creator: null, |
||
timestamp: null, |
timestamp: null, |
||
سطر 2٬445: | سطر 2٬007: | ||
protectMove: null, |
protectMove: null, |
||
protectCreate: null, |
protectCreate: null, |
||
protectCascade: |
protectCascade: false, |
||
// - creation lookup |
// - creation lookup |
||
سطر 2٬459: | سطر 2٬021: | ||
lastEditTime: null, |
lastEditTime: null, |
||
pageID: null, |
pageID: null, |
||
contentModel: null, |
|||
revertCurID: null, |
revertCurID: null, |
||
revertUser: null, |
revertUser: null, |
||
سطر 2٬494: | سطر 2٬055: | ||
patrolProcessApi: null, |
patrolProcessApi: null, |
||
triageApi: null, |
triageApi: null, |
||
triageProcessListApi: null, |
|||
triageProcessApi: null, |
triageProcessApi: null, |
||
deleteApi: null, |
deleteApi: null, |
||
سطر 2٬509: | سطر 2٬069: | ||
/** |
/** |
||
* Loads the text for the page |
* Loads the text for the page |
||
* @param {Function} onSuccess - callback function which is called when the load has succeeded |
|||
* |
|||
* @param {Function} |
* @param {Function} [onFailure] - callback function which is called when the load fails (optional) |
||
* @param {Function} [onFailure] - Callback function which is called when the load fails. |
|||
*/ |
*/ |
||
this.load = function(onSuccess, onFailure) { |
this.load = function(onSuccess, onFailure) { |
||
سطر 2٬528: | سطر 2٬087: | ||
action: 'query', |
action: 'query', |
||
prop: 'info|revisions', |
prop: 'info|revisions', |
||
intestactions: 'edit', // can be expanded |
|||
curtimestamp: '', |
curtimestamp: '', |
||
meta: 'tokens', |
meta: 'tokens', |
||
سطر 2٬558: | سطر 2٬116: | ||
ctx.loadApi.post(); |
ctx.loadApi.post(); |
||
}; |
}; |
||
</syntaxhighlight> |
|||
== الجزء الثاني== |
|||
<syntaxhighlight lang="js" line="1"> |
|||
/** |
/** |
||
* Saves the text for the page to Wikipedia |
* Saves the text for the page to Wikipedia |
||
* Must be preceded by successfully calling |
* Must be preceded by successfully calling load(). |
||
* |
|||
* Warning: Calling `save()` can result in additional calls to the |
|||
* previous `load()` callbacks to recover from edit conflicts! In this |
|||
* case, callers must make the same edit to the new pageText and |
|||
* reinvoke `save()`. This behavior can be disabled with |
|||
* `setMaxConflictRetries(0)`. |
|||
* |
* |
||
* Warning: Calling save() can result in additional calls to the previous load() callbacks |
|||
* @param {Function} [onSuccess] - Callback function which is called when the save has succeeded. |
|||
* to recover from edit conflicts! |
|||
* @param {Function} [onFailure] - Callback function which is called when the save fails. |
|||
* In this case, callers must make the same edit to the new pageText and reinvoke save(). |
|||
* This behavior can be disabled with setMaxConflictRetries(0). |
|||
* @param {Function} [onSuccess] - callback function which is called when the save has |
|||
* succeeded (optional) |
|||
* @param {Function} [onFailure] - callback function which is called when the save fails |
|||
* (optional) |
|||
*/ |
*/ |
||
this.save = function(onSuccess, onFailure) { |
this.save = function(onSuccess, onFailure) { |
||
سطر 2٬585: | سطر 2٬145: | ||
} |
} |
||
if (!ctx.editSummary) { |
if (!ctx.editSummary) { |
||
ctx.statusElement.error('Internal error: edit summary not set before save!'); |
|||
// new section mode allows (nay, encourages) using the |
|||
ctx.onSaveFailure(this); |
|||
// title as the edit summary, but the query needs |
|||
return; |
|||
// editSummary to be undefined or '', not null |
|||
if (ctx.editMode === 'new' && ctx.newSectionTitle) { |
|||
ctx.editSummary = ''; |
|||
} else { |
|||
ctx.statusElement.error('Internal error: edit summary not set before save!'); |
|||
ctx.onSaveFailure(this); |
|||
return; |
|||
} |
|||
} |
} |
||
سطر 2٬616: | سطر 2٬169: | ||
watchlist: ctx.watchlistOption |
watchlist: ctx.watchlistOption |
||
}; |
}; |
||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
|||
if (ctx.watchlistExpiry) { |
|||
query.watchlistexpiry = ctx.watchlistExpiry; |
|||
} |
|||
if (typeof ctx.pageSection === 'number') { |
if (typeof ctx.pageSection === 'number') { |
||
سطر 2٬642: | سطر 2٬188: | ||
switch (ctx.editMode) { |
switch (ctx.editMode) { |
||
case 'append': |
case 'append': |
||
if (ctx.appendText === null) { |
|||
ctx.statusElement.error('Internal error: append text not set before save!'); |
|||
ctx.onSaveFailure(this); |
|||
return; |
|||
} |
|||
query.appendtext = ctx.appendText; // use mode to append to current page contents |
query.appendtext = ctx.appendText; // use mode to append to current page contents |
||
break; |
break; |
||
case 'prepend': |
case 'prepend': |
||
if (ctx.prependText === null) { |
|||
ctx.statusElement.error('Internal error: prepend text not set before save!'); |
|||
ctx.onSaveFailure(this); |
|||
return; |
|||
} |
|||
query.prependtext = ctx.prependText; // use mode to prepend to current page contents |
query.prependtext = ctx.prependText; // use mode to prepend to current page contents |
||
break; |
|||
case 'new': |
|||
if (!ctx.newSectionText) { // API doesn't allow empty new section text |
|||
ctx.statusElement.error('Internal error: new section text not set before save!'); |
|||
ctx.onSaveFailure(this); |
|||
return; |
|||
} |
|||
query.section = 'new'; |
|||
query.text = ctx.newSectionText; // add a new section to current page |
|||
query.sectiontitle = ctx.newSectionTitle || ctx.editSummary; // done by the API, but non-'' values would get treated as text |
|||
break; |
break; |
||
case 'revert': |
case 'revert': |
||
سطر 2٬698: | سطر 2٬224: | ||
/** |
/** |
||
* Adds the text provided via |
* Adds the text provided via setAppendText() to the end of the page. |
||
* Does not require calling |
* Does not require calling load() first. |
||
* @param {Function} [onSuccess] - callback function which is called when the method has succeeded (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function which is called when the method fails (optional) |
||
* @param {Function} [onFailure] - Callback function which is called when the method fails. |
|||
*/ |
*/ |
||
this.append = function(onSuccess, onFailure) { |
this.append = function(onSuccess, onFailure) { |
||
سطر 2٬717: | سطر 2٬242: | ||
/** |
/** |
||
* Adds the text provided via |
* Adds the text provided via setPrependText() to the start of the page. |
||
* Does not require calling |
* Does not require calling load() first. |
||
* @param {Function} [onSuccess] - callback function which is called when the method has succeeded (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function which is called when the method fails (optional) |
||
* @param {Function} [onFailure] - Callback function which is called when the method fails. |
|||
*/ |
*/ |
||
this.prepend = function(onSuccess, onFailure) { |
this.prepend = function(onSuccess, onFailure) { |
||
سطر 2٬735: | سطر 2٬259: | ||
}; |
}; |
||
/** @returns {string} string containing the name of the loaded page, including the namespace */ |
|||
/** |
|||
* Creates a new section with the text provided by `setNewSectionText()` |
|||
* and section title from `setNewSectionTitle()`. |
|||
* If `editSummary` is provided, that will be used instead of the |
|||
* autogenerated "->Title (new section" edit summary. |
|||
* Does not require calling `load()` first. |
|||
* |
|||
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded. |
|||
* @param {Function} [onFailure] - Callback function which is called when the method fails. |
|||
*/ |
|||
this.newSection = function(onSuccess, onFailure) { |
|||
ctx.editMode = 'new'; |
|||
if (fnCanUseMwUserToken('edit')) { |
|||
this.save(onSuccess, onFailure); |
|||
} else { |
|||
ctx.onSaveSuccess = onSuccess; |
|||
ctx.onSaveFailure = onFailure || emptyFunction; |
|||
this.load(fnAutoSave, ctx.onSaveFailure); |
|||
} |
|||
}; |
|||
/** @returns {string} The name of the loaded page, including the namespace */ |
|||
this.getPageName = function() { |
this.getPageName = function() { |
||
return ctx.pageName; |
return ctx.pageName; |
||
}; |
}; |
||
/** @returns {string} |
/** @returns {string} string containing the text of the page after a successful load() */ |
||
this.getPageText = function() { |
this.getPageText = function() { |
||
return ctx.pageText; |
return ctx.pageText; |
||
}; |
}; |
||
/** @param {string} pageText - |
/** @param {string} pageText - updated page text that will be saved when save() is called */ |
||
this.setPageText = function(pageText) { |
this.setPageText = function(pageText) { |
||
ctx.editMode = 'all'; |
ctx.editMode = 'all'; |
||
سطر 2٬773: | سطر 2٬275: | ||
}; |
}; |
||
/** @param {string} appendText - |
/** @param {string} appendText - text that will be appended to the page when append() is called */ |
||
this.setAppendText = function(appendText) { |
this.setAppendText = function(appendText) { |
||
ctx.editMode = 'append'; |
ctx.editMode = 'append'; |
||
سطر 2٬779: | سطر 2٬281: | ||
}; |
}; |
||
/** @param {string} prependText - |
/** @param {string} prependText - text that will be prepended to the page when prepend() is called */ |
||
this.setPrependText = function(prependText) { |
this.setPrependText = function(prependText) { |
||
ctx.editMode = 'prepend'; |
ctx.editMode = 'prepend'; |
||
ctx.prependText = prependText; |
ctx.prependText = prependText; |
||
}; |
|||
/** @param {string} newSectionText - Text that will be added in a new section on the page when `newSection()` is called */ |
|||
this.setNewSectionText = function(newSectionText) { |
|||
ctx.editMode = 'new'; |
|||
ctx.newSectionText = newSectionText; |
|||
}; |
|||
/** |
|||
* @param {string} newSectionTitle - Title for the new section created when `newSection()` is called |
|||
* If missing, `ctx.editSummary` will be used. Issues may occur if a substituted template is used. |
|||
*/ |
|||
this.setNewSectionTitle = function(newSectionTitle) { |
|||
ctx.editMode = 'new'; |
|||
ctx.newSectionTitle = newSectionTitle; |
|||
}; |
}; |
||
سطر 2٬803: | سطر 2٬290: | ||
// Edit-related setter methods: |
// Edit-related setter methods: |
||
/** @param {string} summary - text of the edit summary that will be used when save() is called */ |
|||
/** |
|||
* Set the edit summary that will be used when `save()` is called. |
|||
* Unnecessary if editMode is 'new' and newSectionTitle is provided. |
|||
* |
|||
* @param {string} summary |
|||
*/ |
|||
this.setEditSummary = function(summary) { |
this.setEditSummary = function(summary) { |
||
ctx.editSummary = summary; |
ctx.editSummary = summary; |
||
سطر 2٬814: | سطر 2٬296: | ||
/** |
/** |
||
* @param {string} createOption - can take the following four values: |
|||
* Set any custom tag(s) to be applied to the API action. |
|||
* `recreate` - create the page if it does not exist, or edit it if it exists. |
|||
* A number of actions don't support it, most notably watch, review, |
|||
* `createonly` - create the page if it does not exist, but return an error if it |
|||
* and stabilize ({@link https://phabricator.wikimedia.org/T247721|T247721}), and |
|||
* already exists. |
|||
* pagetriageaction ({@link https://phabricator.wikimedia.org/T252980|T252980}). |
|||
* `nocreate` - don't create the page, only edit it if it already exists. |
|||
* |
|||
* null - create the page if it does not exist, unless it was deleted in the moment |
|||
* @param {string|string[]} tags - String or array of tag(s). |
|||
* between loading the page and saving the edit (default) |
|||
*/ |
|||
this.setChangeTags = function(tags) { |
|||
ctx.changeTags = tags; |
|||
}; |
|||
/** |
|||
* @param {string} [createOption=null] - Can take the following four values: |
|||
* - recreate: create the page if it does not exist, or edit it if it exists. |
|||
* - createonly: create the page if it does not exist, but return an |
|||
* error if it already exists. |
|||
* - nocreate: don't create the page, only edit it if it already exists. |
|||
* - `null`: create the page if it does not exist, unless it was deleted |
|||
* in the moment between loading the page and saving the edit (default). |
|||
* |
* |
||
*/ |
*/ |
||
سطر 2٬840: | سطر 2٬309: | ||
}; |
}; |
||
/** @param {boolean} minorEdit - |
/** @param {boolean} minorEdit - set true to mark the edit as a minor edit. */ |
||
this.setMinorEdit = function(minorEdit) { |
this.setMinorEdit = function(minorEdit) { |
||
ctx.minorEdit = minorEdit; |
ctx.minorEdit = minorEdit; |
||
}; |
}; |
||
/** @param {boolean} botEdit - |
/** @param {boolean} botEdit - set true to mark the edit as a bot edit */ |
||
this.setBotEdit = function(botEdit) { |
this.setBotEdit = function(botEdit) { |
||
ctx.botEdit = botEdit; |
ctx.botEdit = botEdit; |
||
سطر 2٬851: | سطر 2٬320: | ||
/** |
/** |
||
* @param {number} pageSection - |
* @param {number} pageSection - integer specifying the section number to load or save. |
||
* If specified as `null`, the entire page will be retrieved. |
* If specified as `null`, the entire page will be retrieved. |
||
*/ |
*/ |
||
سطر 2٬859: | سطر 2٬328: | ||
/** |
/** |
||
* @param {number} maxConflictRetries - |
* @param {number} maxConflictRetries - number of retries for save errors involving an edit conflict or |
||
* loss of token. Default: 2 |
* loss of token. Default: 2 |
||
*/ |
*/ |
||
this.setMaxConflictRetries = function(maxConflictRetries) { |
this.setMaxConflictRetries = function(maxConflictRetries) { |
||
سطر 2٬867: | سطر 2٬336: | ||
/** |
/** |
||
* @param {number} maxRetries - |
* @param {number} maxRetries - number of retries for save errors not involving an edit conflict or |
||
* loss of token. Default: 2 |
* loss of token. Default: 2 |
||
*/ |
*/ |
||
this.setMaxRetries = function(maxRetries) { |
this.setMaxRetries = function(maxRetries) { |
||
سطر 2٬875: | سطر 2٬344: | ||
/** |
/** |
||
* @param {boolean |
* @param {boolean} watchlistOption |
||
* True - page will be added to the user's watchlist when save() is called |
|||
* Basically a mix of MW API and Twinkley options available pre-expiry: |
|||
* False - watchlist status of the page will not be changed (default) |
|||
* - `true`|`'yes'`: page will be added to the user's watchlist when the action is called |
|||
* - `false`|`'no'`: watchlist status of the page will not be changed. |
|||
* - `'default'`|`'preferences'`: watchlist status of the page will |
|||
* be set based on the user's preference settings when the action is |
|||
* called. Ignores ability of default + expiry. |
|||
* - `'unwatch'`: explicitly unwatch the page |
|||
* - {string|number}: watch page until the specified time (relative or absolute datestring) |
|||
*/ |
*/ |
||
this.setWatchlist = function(watchlistOption) { |
this.setWatchlist = function(watchlistOption) { |
||
if ( |
if (watchlistOption) { |
||
ctx.watchlistOption = 'watch'; |
|||
} else { |
|||
ctx.watchlistOption = 'nochange'; |
ctx.watchlistOption = 'nochange'; |
||
} else if (watchlistOption === 'default' || watchlistOption === 'preferences') { |
|||
ctx.watchlistOption = 'preferences'; |
|||
} else if (watchlistOption === 'unwatch') { |
|||
ctx.watchlistOption = 'unwatch'; |
|||
} else { |
|||
ctx.watchlistOption = 'watch'; |
|||
if (typeof watchlistOption === 'number' || (typeof watchlistOption === 'string' && watchlistOption !== 'yes')) { |
|||
ctx.watchlistExpiry = watchlistOption; |
|||
} |
|||
} |
} |
||
}; |
}; |
||
/** |
/** |
||
* @param {boolean} watchlistOption |
|||
* Set an expiry. setWatchlist can handle this by itself if passed a |
|||
* True - page watchlist status will be set based on the user's |
|||
* string, so this is here largely for completeness and compatibility. |
|||
* preference settings when save() is called. |
|||
* False - watchlist status of the page will not be changed (default) |
|||
* |
* |
||
* Watchlist notes: |
|||
* @param {string} watchlistExpiry |
|||
* 1. The MediaWiki API value of 'unwatch', which explicitly removes the page from the |
|||
* */ |
|||
* user's watchlist, is not used. |
|||
this.setWatchlistExpiry = function(watchlistExpiry) { |
|||
* 2. If both setWatchlist() and setWatchlistFromPreferences() are called, |
|||
ctx.watchlistExpiry = watchlistExpiry; |
|||
* the last call takes priority. |
|||
}; |
|||
* 3. Twinkle modules should use the appropriate preference to set the watchlist options. |
|||
* 4. Most Twinkle modules use setWatchlist(). |
|||
/** |
|||
* setWatchlistFromPreferences() is only needed for the few Twinkle watchlist preferences |
|||
* @deprecated As of December 2020, use setWatchlist. |
|||
* that accept a string value of 'default'. |
|||
* @param {boolean} [watchlistOption=false] - |
|||
* - `True`: page watchlist status will be set based on the user's |
|||
* preference settings when `save()` is called. |
|||
* - `False`: watchlist status of the page will not be changed. |
|||
* |
|||
* Watchlist notes: |
|||
* 1. The MediaWiki API value of 'unwatch', which explicitly removes |
|||
* the page from the user's watchlist, is not used. |
|||
* 2. If both `setWatchlist()` and `setWatchlistFromPreferences()` are |
|||
* called, the last call takes priority. |
|||
* 3. Twinkle modules should use the appropriate preference to set the watchlist options. |
|||
* 4. Most Twinkle modules use `setWatchlist()`. `setWatchlistFromPreferences()` |
|||
* is only needed for the few Twinkle watchlist preferences that |
|||
* accept a string value of `default`. |
|||
*/ |
*/ |
||
this.setWatchlistFromPreferences = function(watchlistOption) { |
this.setWatchlistFromPreferences = function(watchlistOption) { |
||
console.warn('NOTE: Morebits.wiki.page.setWatchlistFromPreferences was deprecated December 2020, please use setWatchlist'); // eslint-disable-line no-console |
|||
if (watchlistOption) { |
if (watchlistOption) { |
||
ctx.watchlistOption = 'preferences'; |
ctx.watchlistOption = 'preferences'; |
||
سطر 2٬937: | سطر 2٬381: | ||
/** |
/** |
||
* @param {boolean} |
* @param {boolean} followRedirect |
||
* |
* true - a maximum of one redirect will be followed. |
||
* 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 |
||
* 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). |
||
* @param {boolean} [followCrossNsRedirect=true] - Not applicable if `followRedirect` is not set true. |
|||
* - `true`: (default) follow redirect even if it is a cross-namespace redirect |
|||
* - `false`: don't follow redirect if it is cross-namespace, edit the redirect itself. |
|||
*/ |
*/ |
||
this.setFollowRedirect = function(followRedirect |
this.setFollowRedirect = function(followRedirect) { |
||
if (ctx.pageLoaded) { |
if (ctx.pageLoaded) { |
||
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!'); |
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!'); |
||
سطر 2٬952: | سطر 2٬393: | ||
} |
} |
||
ctx.followRedirect = followRedirect; |
ctx.followRedirect = followRedirect; |
||
ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect; |
|||
}; |
}; |
||
// lookup-creation setter function |
// lookup-creation setter function |
||
/** |
/** |
||
* @param {boolean} flag - |
* @param {boolean} flag - if set true, the author and timestamp of the first non-redirect |
||
* |
* version of the page is retrieved. |
||
* |
* |
||
* Warning: |
* Warning: |
||
* 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 redirects, the original creation is retrived. |
||
* 2. Revisions that the user is not privileged to access (revdeled/suppressed) will be treated |
|||
* redirects, the original creation is retrived. |
|||
* as non-redirects. |
|||
* 2. Revisions that the user is not privileged to access |
|||
* (revdeled/suppressed) will be treated as non-redirects. |
|||
* 3. Must not be used when the page has a non-wikitext contentmodel |
* 3. Must not be used when the page has a non-wikitext contentmodel |
||
* such as Modulespace Lua or user JavaScript/CSS |
* such as Modulespace Lua or user JavaScript/CSS |
||
*/ |
*/ |
||
this.setLookupNonRedirectCreator = function(flag) { |
this.setLookupNonRedirectCreator = function(flag) { |
||
سطر 2٬995: | سطر 2٬434: | ||
// Protect-related setter functions |
// Protect-related setter functions |
||
/** |
|||
* @param {string} level - The right required for the specific action |
|||
* e.g. autoconfirmed, sysop, templateeditor, extendedconfirmed |
|||
* (enWiki-only). |
|||
* @param {string} [expiry=infinity] |
|||
*/ |
|||
this.setEditProtection = function(level, expiry) { |
this.setEditProtection = function(level, expiry) { |
||
ctx.protectEdit = { level: level, expiry: expiry |
ctx.protectEdit = { level: level, expiry: expiry }; |
||
}; |
}; |
||
this.setMoveProtection = function(level, expiry) { |
this.setMoveProtection = function(level, expiry) { |
||
ctx.protectMove = { level: level, expiry: expiry |
ctx.protectMove = { level: level, expiry: expiry }; |
||
}; |
}; |
||
this.setCreateProtection = function(level, expiry) { |
this.setCreateProtection = function(level, expiry) { |
||
ctx.protectCreate = { level: level, expiry: expiry |
ctx.protectCreate = { level: level, expiry: expiry }; |
||
}; |
}; |
||
سطر 3٬026: | سطر 2٬459: | ||
}; |
}; |
||
/** @returns {string} |
/** @returns {string} string containing the current revision ID of the page */ |
||
this.getCurrentID = function() { |
this.getCurrentID = function() { |
||
return ctx.revertCurID; |
return ctx.revertCurID; |
||
}; |
}; |
||
/** @returns {string} |
/** @returns {string} last editor of the page */ |
||
this.getRevisionUser = function() { |
this.getRevisionUser = function() { |
||
return ctx.revertUser; |
return ctx.revertUser; |
||
}; |
|||
/** @returns {string} ISO 8601 timestamp at which the page was last edited. */ |
|||
this.getLastEditTime = function() { |
|||
return ctx.lastEditTime; |
|||
}; |
}; |
||
سطر 3٬044: | سطر 2٬472: | ||
/** |
/** |
||
* |
* `callbackParameters` - an object for use in a callback function |
||
* |
* |
||
* |
* Callback notes: callbackParameters is for use by the caller only. The parameters |
||
* allow a caller to pass the proper context into its callback |
* allow a caller to pass the proper context into its callback function. |
||
* |
* Callers must ensure that any changes to the callbackParameters object |
||
* |
* within a load() callback still permit a proper re-entry into the |
||
* |
* load() callback if an edit conflict is detected upon calling save(). |
||
* detected upon calling `save()`. |
|||
* |
|||
* @param {object} callbackParameters |
|||
*/ |
*/ |
||
this.setCallbackParameters = function(callbackParameters) { |
this.setCallbackParameters = function(callbackParameters) { |
||
سطر 3٬060: | سطر 2٬485: | ||
/** |
/** |
||
* @returns |
* @returns the object previous set by setCallbackParameters() |
||
*/ |
*/ |
||
this.getCallbackParameters = function() { |
this.getCallbackParameters = function() { |
||
سطر 3٬067: | سطر 2٬492: | ||
/** |
/** |
||
* @returns {Morebits.status} Status element created by the constructor |
* @returns {Morebits.status} Status element created by the constructor |
||
*/ |
*/ |
||
this.getStatusElement = function() { |
this.getStatusElement = function() { |
||
سطر 3٬074: | سطر 2٬499: | ||
/** |
/** |
||
* @param {string} level |
* @param {string} level The right required for edits not to require |
||
* review. Possible options: none, autoconfirmed, review (not on enWiki). |
* review. Possible options: none, autoconfirmed, review (not on enWiki). |
||
* @param {string} |
* @param {string} expiry |
||
*/ |
*/ |
||
this.setFlaggedRevs = function(level, expiry) { |
this.setFlaggedRevs = function(level, expiry) { |
||
ctx.flaggedRevs = { level: level, expiry: expiry |
ctx.flaggedRevs = { level: level, expiry: expiry }; |
||
}; |
}; |
||
/** |
/** |
||
* @returns {boolean} |
* @returns {boolean} true if the page existed on the wiki when it was last loaded |
||
*/ |
*/ |
||
this.exists = function() { |
this.exists = function() { |
||
سطر 3٬098: | سطر 2٬523: | ||
/** |
/** |
||
* @returns {string} |
* @returns {string} ISO 8601 timestamp at which the page was last loaded |
||
* include (but may not be limited to): `wikitext`, `javascript`, |
|||
* `css`, `json`, `Scribunto`, `sanitized-css`, `MassMessageListContent`. |
|||
* Also gettable via `mw.config.get('wgPageContentModel')`. |
|||
*/ |
|||
this.getContentModel = function() { |
|||
return ctx.contentModel; |
|||
}; |
|||
/** |
|||
* @returns {string} ISO 8601 timestamp at which the page was last loaded. |
|||
*/ |
*/ |
||
this.getLoadTime = function() { |
this.getLoadTime = function() { |
||
سطر 3٬115: | سطر 2٬530: | ||
/** |
/** |
||
* @returns {string} |
* @returns {string} the user who created the page following lookupCreation() |
||
*/ |
*/ |
||
this.getCreator = function() { |
this.getCreator = function() { |
||
سطر 3٬122: | سطر 2٬537: | ||
/** |
/** |
||
* @returns {string} |
* @returns {string} the ISOString timestamp of page creation following lookupCreation() |
||
*/ |
*/ |
||
this.getCreationTimestamp = function() { |
this.getCreationTimestamp = function() { |
||
return ctx.timestamp; |
return ctx.timestamp; |
||
}; |
|||
/** @returns {boolean} whether or not you can edit the page */ |
|||
this.canEdit = function() { |
|||
return !!ctx.testActions && ctx.testActions.indexOf('edit') !== -1; |
|||
}; |
}; |
||
/** |
/** |
||
* Retrieves the username of the user who created the page as well as |
* Retrieves the username of the user who created the page as well as |
||
* the timestamp of creation |
* the timestamp of creation |
||
* @param {Function} onSuccess - callback function (required) which is |
|||
* `getCreator()` function; the timestamp can be retrieved using the |
|||
* called when the username and timestamp are found within the callback. |
|||
* `getCreationTimestamp()` function. |
|||
* The username can be retrieved using the getCreator() function; |
|||
* Prior to June 2019 known as `lookupCreator()`. |
|||
* the timestamp can be retrieved using the getCreationTimestamp() function |
|||
* |
|||
* Prior to June 2019 known as lookupCreator |
|||
* @param {Function} onSuccess - Callback function to be called when |
|||
* the username and timestamp are found within the callback. |
|||
*/ |
*/ |
||
this.lookupCreation = function(onSuccess) { |
this.lookupCreation = function(onSuccess) { |
||
سطر 3٬162: | سطر 2٬571: | ||
// rvsection, others return an error when paired with the |
// rvsection, others return an error when paired with the |
||
// content rvprop. Relatedly, non-wikitext models don't |
// content rvprop. Relatedly, non-wikitext models don't |
||
// understand the # |
// understand the #تحويل concept, so we shouldn't attempt |
||
// the redirect resolution in fnLookupCreationSuccess |
// the redirect resolution in fnLookupCreationSuccess |
||
if (ctx.lookupNonRedirectCreator) { |
if (ctx.lookupNonRedirectCreator) { |
||
سطر 3٬179: | سطر 2٬588: | ||
/** |
/** |
||
* Reverts a page to |
* Reverts a page to revertOldID |
||
* @param {Function} [onSuccess] - callback function to run on success (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function to run on failure (optional) |
||
* @param {Function} [onFailure] - Callback function to run on failure. |
|||
*/ |
*/ |
||
this.revert = function(onSuccess, onFailure) { |
this.revert = function(onSuccess, onFailure) { |
||
سطر 3٬199: | سطر 2٬607: | ||
/** |
/** |
||
* Moves a page to another title |
* Moves a page to another title |
||
* @param {Function} [onSuccess] - callback function to run on success (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function to run on failure (optional) |
||
* @param {Function} [onFailure] - Callback function to run on failure. |
|||
*/ |
*/ |
||
this.move = function(onSuccess, onFailure) { |
this.move = function(onSuccess, onFailure) { |
||
سطر 3٬208: | سطر 2٬615: | ||
ctx.onMoveFailure = onFailure || emptyFunction; |
ctx.onMoveFailure = onFailure || emptyFunction; |
||
if (! |
if (!ctx.editSummary) { |
||
ctx.statusElement.error('Internal error: move reason not set before move (use setEditSummary function)!'); |
|||
return; // abort |
|||
ctx.onMoveFailure(this); |
|||
return; |
|||
} |
} |
||
if (!ctx.moveDestination) { |
if (!ctx.moveDestination) { |
||
ctx.statusElement.error('Internal error: destination page name was not set before move!'); |
ctx.statusElement.error('Internal error: destination page name was not set before move!'); |
||
سطر 3٬230: | سطر 2٬638: | ||
/** |
/** |
||
* Marks the page as patrolled, using |
* Marks the page as patrolled, using rcid (if available) or revid |
||
* |
* |
||
* Patrolling as such doesn't need to rely on loading the page in |
* Patrolling as such doesn't need to rely on loading the page in |
||
* question; simply passing a revid to the API is sufficient, so in |
* question; simply passing a revid to the API is sufficient, so in |
||
* those cases just using |
* those cases just using Morebits.wiki.api is probably preferable. |
||
* |
* |
||
* No error handling since we don't actually care about the errors |
* No error handling since we don't actually care about the errors |
||
*/ |
*/ |
||
this.patrol = function() { |
this.patrol = function() { |
||
سطر 3٬243: | سطر 2٬651: | ||
} |
} |
||
// If |
// If patrolling current page, don't need to query for latest revID |
||
if ($('.patrollink').length) { |
if ($('.patrollink').length) { |
||
var patrolhref = $('.patrollink a').attr('href'); |
var patrolhref = $('.patrollink a').attr('href'); |
||
ctx.rcid = mw.util.getParamValue('rcid', patrolhref); |
ctx.rcid = mw.util.getParamValue('rcid', patrolhref); |
||
fnProcessPatrol(this, this); |
|||
} else if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) { |
|||
ctx.revid = mw.config.get('wgRevisionId') || mw.config.get('wgCurRevisionId'); |
|||
fnProcessPatrol(this, this); |
fnProcessPatrol(this, this); |
||
} else { |
} else { |
||
سطر 3٬254: | سطر 2٬665: | ||
meta: 'tokens', |
meta: 'tokens', |
||
type: 'patrol', // as long as we're querying, might as well get a token |
type: 'patrol', // as long as we're querying, might as well get a token |
||
titles: ctx.pageName |
|||
list: 'recentchanges', // check if the page is unpatrolled |
|||
titles: ctx.pageName, |
|||
rcprop: 'patrolled', |
|||
rctitle: ctx.pageName, |
|||
rclimit: 1 |
|||
}; |
}; |
||
سطر 3٬268: | سطر 2٬675: | ||
/** |
/** |
||
* Marks the page as reviewed by the PageTriage extension |
* Marks the page as reviewed by the PageTriage extension |
||
* https://www.mediawiki.org/wiki/Extension:PageTriage |
|||
* |
* |
||
* Referred to as "review" on-wiki |
|||
* Will, by it's nature, mark as patrolled as well. Falls back to |
|||
* |
|||
* patrolling if not in an appropriate namespace. |
|||
* Will, by it's nature, mark as patrolled as well. |
|||
* |
* |
||
* Doesn't inherently rely on loading the page in question; simply |
* Doesn't inherently rely on loading the page in question; simply |
||
* passing a |
* passing a pageid to the API is sufficient, so in those cases just |
||
* using |
* using Morebits.wiki.api is probably preferable. |
||
* |
|||
* Will first check if the page is queued via {@link |
|||
* Morebits.wiki.page~fnProcessTriageList|fnProcessTriageList}. |
|||
* |
|||
* No error handling since we don't actually care about the errors. |
|||
* |
* |
||
* No error handling since we don't actually care about the errors |
|||
* @see {@link https://www.mediawiki.org/wiki/Extension:PageTriage} Referred to as "review" on-wiki. |
|||
*/ |
*/ |
||
this.triage = function() { |
this.triage = function() { |
||
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) { |
|||
// Fall back to patrol if not a valid triage namespace |
|||
return; |
|||
if (mw.config.get('pageTriageNamespaces').indexOf(new mw.Title(ctx.pageName).getNamespaceId()) === -1) { |
|||
} |
|||
this.patrol(); |
|||
// If on the page in question, don't need to query for page ID |
|||
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) { |
|||
ctx.pageID = mw.config.get('wgArticleId'); |
|||
fnProcessTriage(this, this); |
|||
} else { |
} else { |
||
var query = fnNeedTokenInfoQuery('triage'); |
|||
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) { |
|||
return; |
|||
} |
|||
ctx.triageApi = new Morebits.wiki.api('retrieving token...', query, fnProcessTriage); |
|||
// If on the page in question, don't need to query for page ID |
|||
ctx.triageApi.setParent(this); |
|||
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) { |
|||
ctx.triageApi.post(); |
|||
fnProcessTriageList(this, this); |
|||
} else { |
|||
var query = fnNeedTokenInfoQuery('triage'); |
|||
ctx.triageApi = new Morebits.wiki.api('retrieving token...', query, fnProcessTriageList); |
|||
ctx.triageApi.setParent(this); |
|||
ctx.triageApi.post(); |
|||
} |
|||
} |
} |
||
}; |
}; |
||
سطر 3٬309: | سطر 2٬708: | ||
// |delete| is a reserved word in some flavours of JS |
// |delete| is a reserved word in some flavours of JS |
||
/** |
/** |
||
* Deletes a page (for admins only) |
* Deletes a page (for admins only) |
||
* @param {Function} [onSuccess] - callback function to run on success (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function to run on failure (optional) |
||
* @param {Function} [onFailure] - Callback function to run on failure. |
|||
*/ |
*/ |
||
this.deletePage = function(onSuccess, onFailure) { |
this.deletePage = function(onSuccess, onFailure) { |
||
سطر 3٬318: | سطر 2٬716: | ||
ctx.onDeleteFailure = onFailure || emptyFunction; |
ctx.onDeleteFailure = onFailure || emptyFunction; |
||
// if a non-admin tries to do this, don't bother |
|||
if (!fnPreflightChecks.call(this, 'delete', ctx.onDeleteFailure)) { |
|||
if (!Morebits.userIsSysop) { |
|||
return; // abort |
|||
ctx.statusElement.error('لا يمكن حذف الصفحة: يمكن للإداريين فقط القيام بذلك'); |
|||
ctx.onDeleteFailure(this); |
|||
return; |
|||
} |
|||
if (!ctx.editSummary) { |
|||
ctx.statusElement.error('خطأ داخلي: لم يتم تحديد سبب الحذف قبل الحذف (استخدم دالة setEditSummary)'); |
|||
ctx.onDeleteFailure(this); |
|||
return; |
|||
} |
} |
||
سطر 3٬334: | سطر 2٬740: | ||
/** |
/** |
||
* Undeletes a page (for admins only) |
* Undeletes a page (for admins only) |
||
* @param {Function} [onSuccess] - callback function to run on success (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function to run on failure (optional) |
||
* @param {Function} [onFailure] - Callback function to run on failure. |
|||
*/ |
*/ |
||
this.undeletePage = function(onSuccess, onFailure) { |
this.undeletePage = function(onSuccess, onFailure) { |
||
سطر 3٬343: | سطر 2٬748: | ||
ctx.onUndeleteFailure = onFailure || emptyFunction; |
ctx.onUndeleteFailure = onFailure || emptyFunction; |
||
// if a non-admin tries to do this, don't bother |
|||
if (!fnPreflightChecks.call(this, 'undelete', ctx.onUndeleteFailure)) { |
|||
if (!Morebits.userIsSysop) { |
|||
return; // abort |
|||
ctx.statusElement.error('Cannot undelete page: only admins can do that'); |
|||
ctx.onUndeleteFailure(this); |
|||
return; |
|||
} |
|||
if (!ctx.editSummary) { |
|||
ctx.statusElement.error('Internal error: undelete reason not set before undelete (use setEditSummary function)!'); |
|||
ctx.onUndeleteFailure(this); |
|||
return; |
|||
} |
} |
||
سطر 3٬359: | سطر 2٬772: | ||
/** |
/** |
||
* Protects a page (for admins only) |
* Protects a page (for admins only) |
||
* @param {Function} [onSuccess] - callback function to run on success (optional) |
|||
* |
|||
* @param {Function} [ |
* @param {Function} [onFailure] - callback function to run on failure (optional) |
||
* @param {Function} [onFailure] - Callback function to run on failure. |
|||
*/ |
*/ |
||
this.protect = function(onSuccess, onFailure) { |
this.protect = function(onSuccess, onFailure) { |
||
سطر 3٬368: | سطر 2٬780: | ||
ctx.onProtectFailure = onFailure || emptyFunction; |
ctx.onProtectFailure = onFailure || emptyFunction; |
||
// if a non-admin tries to do this, don't bother |
|||
if (!fnPreflightChecks.call(this, 'protect', ctx.onProtectFailure)) { |
|||
if (!Morebits.userIsSysop) { |
|||
return; // abort |
|||
ctx.statusElement.error('Cannot protect page: only admins can do that'); |
|||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
} |
||
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) { |
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) { |
||
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!'); |
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!'); |
||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
|||
if (!ctx.editSummary) { |
|||
ctx.statusElement.error('Internal error: protection reason not set before protect (use setEditSummary function)!'); |
|||
ctx.onProtectFailure(this); |
ctx.onProtectFailure(this); |
||
return; |
return; |
||
سطر 3٬389: | سطر 2٬808: | ||
/** |
/** |
||
* Apply FlaggedRevs protection settings |
* Apply FlaggedRevs protection-style settings |
||
* |
* only works where $wgFlaggedRevsProtection = true (i.e. where FlaggedRevs |
||
* |
* settings appear on the wiki's "protect" tab) |
||
* @param {function} [onSuccess] |
|||
* |
|||
* @param {function} [onFailure] |
|||
* @see {@link https://www.mediawiki.org/wiki/Extension:FlaggedRevs} |
|||
* Referred to as "pending changes" on-wiki. |
|||
* |
|||
* @param {Function} [onSuccess] |
|||
* @param {Function} [onFailure] |
|||
*/ |
*/ |
||
this.stabilize = function(onSuccess, onFailure) { |
this.stabilize = function(onSuccess, onFailure) { |
||
سطر 3٬403: | سطر 2٬818: | ||
ctx.onStabilizeFailure = onFailure || emptyFunction; |
ctx.onStabilizeFailure = onFailure || emptyFunction; |
||
// if a non-admin tries to do this, don't bother |
|||
if (!fnPreflightChecks.call(this, 'FlaggedRevs', ctx.onStabilizeFailure)) { |
|||
if (!Morebits.userIsSysop) { |
|||
return; // abort |
|||
ctx.statusElement.error('Cannot apply FlaggedRevs settings: only admins can do that'); |
|||
ctx.onStabilizeFailure(this); |
|||
return; |
|||
} |
} |
||
if (!ctx.flaggedRevs) { |
if (!ctx.flaggedRevs) { |
||
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!'); |
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!'); |
||
ctx.onStabilizeFailure(this); |
|||
return; |
|||
} |
|||
if (!ctx.editSummary) { |
|||
ctx.statusElement.error('Internal error: reason not set before calling stabilize() (use setEditSummary function)!'); |
|||
ctx.onStabilizeFailure(this); |
ctx.onStabilizeFailure(this); |
||
return; |
return; |
||
سطر 3٬433: | سطر 2٬855: | ||
* HTML, or whether we need to ask the server for more info (e.g. protection expiry). |
* HTML, or whether we need to ask the server for more info (e.g. protection expiry). |
||
* |
* |
||
* Only applicable for csrf token actions, e.g. not patrol |
|||
* Currently used for `append`, `prepend`, `newSection`, `move`, |
|||
* `stabilize`, `deletePage`, and `undeletePage`. Can't use for |
|||
* `protect` since it always needs to request protection status. |
|||
* |
* |
||
* Currently used for append, prepend, deletePage, undeletePage, move, |
|||
* @param {string} [action=edit] - The action being undertaken, e.g. |
|||
* and stabilize. Can't use for protect since it always needs to |
|||
* request protection status. |
|||
* |
|||
* @param {string} [action=edit] The action being undertaken, e.g. |
|||
* "edit" or "delete". In practice, only "edit" or "notedit" matters. |
* "edit" or "delete". In practice, only "edit" or "notedit" matters. |
||
* @returns {boolean} |
* @returns {boolean} |
||
سطر 3٬445: | سطر 2٬869: | ||
// API-based redirect resolution only works for action=query and |
// API-based redirect resolution only works for action=query and |
||
// action=edit in append/prepend |
// action=edit in append/prepend modes (and section=new, but we don't |
||
// really support that) |
|||
if (ctx.followRedirect) { |
|||
if (ctx.followRedirect && (action !== 'edit' || |
|||
(ctx.editMode !== 'append' && ctx.editMode !== 'prepend'))) { |
|||
return false; // must load the page to check for cross namespace redirects |
|||
return false; |
|||
} |
|||
if (action !== 'edit' || (ctx.editMode === 'all' || ctx.editMode === 'revert')) { |
|||
return false; |
|||
} |
|||
} |
} |
||
// do we need to fetch the edit protection expiry? |
// do we need to fetch the edit protection expiry? |
||
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) { |
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) { |
||
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() |
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) { |
||
return false; |
return false; |
||
} |
} |
||
سطر 3٬473: | سطر 2٬894: | ||
/** |
/** |
||
* When functions can't use |
* When functions can't use fnCanUseMwUserToken or require checking |
||
* protection, maintain the query in one place. Used for delete, |
|||
* Morebits.wiki.page~fnCanUseMwUserToken|fnCanUseMwUserToken} or |
|||
* undelete, protect, stabilize, and move (basically, just not load) |
|||
* require checking protection, maintain the query in one place. Used |
|||
* for {@link Morebits.wiki.page#deletePage|delete}, {@link |
|||
* Morebits.wiki.page#undeletePage|undelete}, {@link |
|||
* Morebits.wiki.page#protect|protect}, {@link |
|||
* Morebits.wiki.page#stabilize|stabilize}, and {@link |
|||
* Morebits.wiki.page#move|move} (basically, just not {@link |
|||
* Morebits.wiki.page#load|load}). |
|||
* |
* |
||
* @param {string} action |
* @param {string} action The action being undertaken, e.g. "edit" or |
||
* "delete" |
* "delete" |
||
* @returns {object} Appropriate token query. |
|||
*/ |
*/ |
||
var fnNeedTokenInfoQuery = function(action) { |
var fnNeedTokenInfoQuery = function(action) { |
||
سطر 3٬505: | سطر 2٬919: | ||
}; |
}; |
||
// callback from loadSuccess() for append() |
// callback from loadSuccess() for append() and prepend() threads |
||
var fnAutoSave = function(pageobj) { |
var fnAutoSave = function(pageobj) { |
||
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure); |
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure); |
||
سطر 3٬538: | سطر 2٬952: | ||
return; |
return; |
||
} |
} |
||
ctx.contentModel = $(xml).find('page').attr('contentmodel'); |
|||
// extract protection info, to alert admins when they are about to edit a protected page |
// extract protection info, to alert admins when they are about to edit a protected page |
||
سطر 3٬553: | سطر 2٬965: | ||
ctx.lastEditTime = $(xml).find('rev').attr('timestamp'); |
ctx.lastEditTime = $(xml).find('rev').attr('timestamp'); |
||
ctx.revertCurID = $(xml).find('page').attr('lastrevid'); |
ctx.revertCurID = $(xml).find('page').attr('lastrevid'); |
||
var testactions = $(xml).find('actions'); |
|||
if (testactions.length) { |
|||
ctx.testActions = []; // was null |
|||
$.each(testactions[0].attributes, function(_idx, value) { |
|||
ctx.testActions.push(value.name); |
|||
}); |
|||
} |
|||
if (ctx.editMode === 'revert') { |
if (ctx.editMode === 'revert') { |
||
سطر 3٬606: | سطر 3٬010: | ||
var resolvedName = $(xml).find('page').attr('title'); |
var resolvedName = $(xml).find('page').attr('title'); |
||
// only notify user for redirects, not normalization |
|||
if ($(xml).find('redirects').length > 0) { |
if ($(xml).find('redirects').length > 0) { |
||
// check for cross-namespace redirect: |
|||
var origNs = new mw.Title(ctx.pageName).namespace; |
|||
var newNs = new mw.Title(resolvedName).namespace; |
|||
if (origNs !== newNs && !ctx.followCrossNsRedirect) { |
|||
ctx.statusElement.error(ctx.pageName + ' is a cross-namespace redirect to ' + resolvedName + ', aborted'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
// only notify user for redirects, not normalization |
|||
Morebits.status.info('Info', 'Redirected from ' + ctx.pageName + ' to ' + resolvedName); |
Morebits.status.info('Info', 'Redirected from ' + ctx.pageName + ' to ' + resolvedName); |
||
} |
} |
||
ctx.pageName = resolvedName; // always update in case of normalization |
|||
ctx.pageName = resolvedName; // update to redirect target or normalized name |
|||
} else { |
} else { |
||
// could be a circular redirect or other problem |
// could be a circular redirect or other problem |
||
سطر 3٬632: | سطر 3٬025: | ||
} |
} |
||
return true; // all OK |
return true; // all OK |
||
}; |
|||
// helper function to get a new token on encountering token errors |
|||
// in save, deletePage, and undeletePage |
|||
// Being a synchronous ajax call, this blocks the event loop, |
|||
// and hence should be used sparingly. |
|||
var fnGetToken = function() { |
|||
var token; |
|||
var tokenApi = new Morebits.wiki.api('Getting token', { |
|||
action: 'query', |
|||
meta: 'tokens' |
|||
}, function(apiobj) { |
|||
token = $(apiobj.responseXML).find('tokens').attr('csrftoken'); |
|||
}, null, function() { |
|||
this.getStatusElement().error('Failed to get token'); |
|||
}); |
|||
tokenApi.post({async: false}); |
|||
return token; |
|||
}; |
}; |
||
// callback from saveApi.post() |
// callback from saveApi.post() |
||
var fnSaveSuccess = function() { |
var fnSaveSuccess = function() { |
||
ctx.editMode = 'all'; // cancel append/prepend |
ctx.editMode = 'all'; // cancel append/prepend/revert modes |
||
var xml = ctx.saveApi.getXML(); |
var xml = ctx.saveApi.getXML(); |
||
سطر 3٬681: | سطر 3٬092: | ||
}; |
}; |
||
var purgeApi = new Morebits.wiki.api('Edit conflict detected, purging server cache', purgeQuery, |
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'); |
|||
if (fnCanUseMwUserToken('edit')) { |
|||
ctx.statusElement.info('Edit conflict detected, reapplying edit'); |
|||
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired |
|||
if (fnCanUseMwUserToken('edit')) { |
|||
} else { |
|||
ctx.saveApi.post(); // necessarily append or prepend, so this should work as desired |
|||
} else { |
|||
ctx.loadApi.post(); // reload the page and reapply the edit |
|||
}, ctx.statusElement); |
|||
} |
|||
purgeApi.post(); |
|||
// check for loss of edit token |
|||
} else if (errorCode === 'badtoken' && ctx.retries++ < ctx.maxRetries) { |
|||
ctx.statusElement.info('Edit token is invalid, retrying'); |
|||
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
|||
ctx.saveApi.query.token = fnGetToken.call(this); |
|||
ctx.saveApi.post(); |
|||
// check for network or server error |
// check for network or server error |
||
} else if |
} else if (errorCode === 'undefined' && ctx.retries++ < ctx.maxRetries) { |
||
// the error might be transient, so try again |
// the error might be transient, so try again |
||
ctx.statusElement.info('Save failed, retrying |
ctx.statusElement.info('Save failed, retrying'); |
||
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
||
ctx.saveApi.post(); // give it another go! |
|||
// wait for sometime for client to regain connnectivity |
|||
sleep(2000).then(function() { |
|||
ctx.saveApi.post(); // give it another go! |
|||
}); |
|||
// hard error, give up |
// hard error, give up |
||
} else { |
} else { |
||
// non-admin attempting to edit a protected page - this gives a friendlier message than the default |
|||
switch (errorCode) { |
|||
if (errorCode === 'protectedpage') { |
|||
ctx.statusElement.error('Failed to save edit: Page is protected'); |
|||
case 'protectedpage': |
|||
// check for absuefilter hits: disallowed or warning |
|||
// non-admin attempting to edit a protected page - this gives a friendlier message than the default |
|||
} else if (errorCode.indexOf('abusefilter') === 0) { |
|||
ctx.statusElement.error('Failed to save edit: Page is protected'); |
|||
var desc = $(ctx.saveApi.getXML()).find('abusefilter').attr('description'); |
|||
break; |
|||
if (errorCode === 'abusefilter-disallowed') { |
|||
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + desc + '".'); |
|||
case 'abusefilter-disallowed': |
|||
} else if (errorCode === 'abusefilter-warning') { |
|||
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + $(ctx.saveApi.getXML()).find('abusefilter').attr('description') + '".'); |
|||
ctx.statusElement.error([ 'A warning was returned by the edit filter: "', desc, '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.' ]); |
|||
break; |
|||
case 'abusefilter-warning': |
|||
ctx.statusElement.error([ 'A warning was returned by the edit filter: "', $(ctx.saveApi.getXML()).find('abusefilter').attr('description'), '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.' ]); |
|||
// We should provide the user with a way to automatically retry the action if they so choose - |
// We should provide the user with a way to automatically retry the action if they so choose - |
||
// I can't see how to do this without creating a UI dependency on Morebits.wiki.page though -- TTO |
// I can't see how to do this without creating a UI dependency on Morebits.wiki.page though -- TTO |
||
} else { // shouldn't happen but... |
|||
break; |
|||
ctx.statusElement.error('The edit was disallowed by the edit filter.'); |
|||
} |
|||
case 'spamblacklist': |
|||
// check for blacklist hits |
|||
// .find('matches') returns an array in case multiple items are blacklisted, we only return the first |
|||
} else if (errorCode === 'spamblacklist') { |
|||
var spam = $(ctx.saveApi.getXML()).find('spamblacklist').find('matches').children()[0].textContent; |
|||
// .find('matches') returns an array in case multiple items are blacklisted, we only return the first |
|||
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist'); |
|||
var spam = $(ctx.saveApi.getXML()).find('spamblacklist').find('matches').children()[0].textContent; |
|||
break; |
|||
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist'); |
|||
} else { |
|||
ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText()); |
|||
} |
} |
||
ctx.editMode = 'all'; // cancel append/prepend/revert modes |
|||
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes |
|||
if (ctx.onSaveFailure) { |
if (ctx.onSaveFailure) { |
||
ctx.onSaveFailure(this); // invoke callback |
ctx.onSaveFailure(this); // invoke callback |
||
سطر 3٬749: | سطر 3٬160: | ||
} |
} |
||
if (!ctx.lookupNonRedirectCreator || !/^\s*# |
if (!ctx.lookupNonRedirectCreator || !/^\s*#تحويل/i.test($(xml).find('rev').text())) { |
||
ctx.creator = $(xml).find('rev').attr('user'); |
ctx.creator = $(xml).find('rev').attr('user'); |
||
سطر 3٬778: | سطر 3٬189: | ||
$(xml).find('rev').each(function(_, rev) { |
$(xml).find('rev').each(function(_, rev) { |
||
if (!/^\s*# |
if (!/^\s*#تحويل/i.test(rev.textContent)) { // inaccessible revisions also check out |
||
ctx.creator = rev.getAttribute('user'); |
ctx.creator = rev.getAttribute('user'); |
||
ctx.timestamp = rev.getAttribute('timestamp'); |
ctx.timestamp = rev.getAttribute('timestamp'); |
||
سطر 3٬802: | سطر 3٬213: | ||
ctx.onLookupCreationSuccess(this); |
ctx.onLookupCreationSuccess(this); |
||
}; |
|||
/** |
|||
* Common checks for action methods. Used for move, undelete, delete, |
|||
* protect, stabilize |
|||
* |
|||
* @param {string} action - The action being checked. |
|||
* @param {string} onFailure - Failure callback. |
|||
* @returns {boolean} |
|||
*/ |
|||
var fnPreflightChecks = function(action, onFailure) { |
|||
// if a non-admin tries to do this, don't bother |
|||
if (!Morebits.userIsSysop && action !== 'move') { |
|||
ctx.statusElement.error('Cannot ' + action + 'page : only admins can do that'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
if (!ctx.editSummary) { |
|||
ctx.statusElement.error('Internal error: ' + action + ' reason not set (use setEditSummary function)!'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
return true; // all OK |
|||
}; |
|||
/** |
|||
* Common checks for fnProcess functions (`fnProcessDelete`, `fnProcessMove`, etc. |
|||
* Used for move, undelete, delete, protect, stabilize. |
|||
* |
|||
* @param {string} action - The action being checked. |
|||
* @param {string} onFailure - Failure callback. |
|||
* @param {string} xml - The response document from the API call. |
|||
* @returns {boolean} |
|||
*/ |
|||
var fnProcessChecks = function(action, onFailure, xml) { |
|||
var missing = $(xml).find('page').attr('missing') === ''; |
|||
// No undelete as an existing page could have deleted revisions |
|||
var actionMissing = missing && ['delete', 'stabilize', 'move'].indexOf(action) !== -1; |
|||
var protectMissing = action === 'protect' && missing && (ctx.protectEdit || ctx.protectMove); |
|||
var saltMissing = action === 'protect' && !missing && ctx.protectCreate; |
|||
if (actionMissing || protectMissing || saltMissing) { |
|||
ctx.statusElement.error('Cannot ' + action + ' the page because it ' + (missing ? 'no longer' : 'already') + ' exists'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
// Delete, undelete, move |
|||
// extract protection info |
|||
var editprot; |
|||
if (action === 'undelete') { |
|||
editprot = $(xml).find('pr[type="create"]'); |
|||
} else if (action === 'delete' || action === 'move') { |
|||
editprot = $(xml).find('pr[type="edit"]'); |
|||
} |
|||
if (editprot && editprot.length && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning && |
|||
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName + |
|||
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') + |
|||
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) { |
|||
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
if (!$(xml).find('tokens').attr('csrftoken')) { |
|||
ctx.statusElement.error('Failed to retrieve token.'); |
|||
onFailure(this); |
|||
return false; |
|||
} |
|||
return true; // all OK |
|||
}; |
}; |
||
سطر 3٬885: | سطر 3٬224: | ||
var xml = ctx.moveApi.getXML(); |
var xml = ctx.moveApi.getXML(); |
||
if ( |
if ($(xml).find('page').attr('missing') === '') { |
||
ctx.statusElement.error('Cannot move the page, because it no longer exists'); |
|||
return; // abort |
|||
ctx.onMoveFailure(this); |
|||
return; |
|||
} |
|||
// extract protection info |
|||
if (Morebits.userIsSysop) { |
|||
var editprot = $(xml).find('pr[type="edit"]'); |
|||
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning && |
|||
!confirm('You are about to move the fully protected page "' + ctx.pageName + |
|||
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') + |
|||
'. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) { |
|||
ctx.statusElement.error('Move of fully protected page was aborted.'); |
|||
ctx.onMoveFailure(this); |
|||
return; |
|||
} |
|||
} |
} |
||
token = $(xml).find('tokens').attr('csrftoken'); |
token = $(xml).find('tokens').attr('csrftoken'); |
||
if (!token) { |
|||
ctx.statusElement.error('Failed to retrieve move token.'); |
|||
ctx.onMoveFailure(this); |
|||
return; |
|||
} |
|||
pageTitle = $(xml).find('page').attr('title'); |
pageTitle = $(xml).find('page').attr('title'); |
||
} |
} |
||
سطر 3٬901: | سطر 3٬261: | ||
'watchlist': ctx.watchlistOption |
'watchlist': ctx.watchlistOption |
||
}; |
}; |
||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
|||
if (ctx.watchlistExpiry) { |
|||
query.watchlistexpiry = ctx.watchlistExpiry; |
|||
} |
|||
if (ctx.moveTalkPage) { |
if (ctx.moveTalkPage) { |
||
query.movetalk = 'true'; |
query.movetalk = 'true'; |
||
سطر 3٬931: | سطر 3٬284: | ||
if (ctx.rcid) { |
if (ctx.rcid) { |
||
query.rcid = ctx.rcid; |
query.rcid = ctx.rcid; |
||
query.token = mw.user.tokens.get('patrolToken'); |
|||
} else if (ctx.revid) { |
|||
query.revid = ctx.revid; |
|||
query.token = mw.user.tokens.get('patrolToken'); |
query.token = mw.user.tokens.get('patrolToken'); |
||
} else { |
} else { |
||
var xml = ctx.patrolApi.getResponse(); |
var xml = ctx.patrolApi.getResponse(); |
||
// Don't patrol if not unpatrolled |
|||
if ($(xml).find('rc').attr('unpatrolled') !== '') { |
|||
return; |
|||
} |
|||
var lastrevid = $(xml).find('page').attr('lastrevid'); |
var lastrevid = $(xml).find('page').attr('lastrevid'); |
||
سطر 3٬952: | سطر 3٬303: | ||
query.token = token; |
query.token = token; |
||
} |
|||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
} |
||
سطر 3٬964: | سطر 3٬312: | ||
}; |
}; |
||
var fnProcessTriage = function() { |
|||
// Ensure that the page is curatable |
|||
var pageID, token; |
|||
var fnProcessTriageList = function() { |
|||
if (ctx.pageID) { |
if (ctx.pageID) { |
||
token = mw.user.tokens.get('csrfToken'); |
|||
pageID = ctx.pageID; |
|||
} else { |
} else { |
||
var xml = ctx.triageApi.getXML(); |
var xml = ctx.triageApi.getXML(); |
||
pageID = $(xml).find('page').attr('pageid'); |
|||
if (! |
if (!pageID) { |
||
return; |
return; |
||
} |
} |
||
token = $(xml).find('tokens').attr('csrftoken'); |
|||
if (! |
if (!token) { |
||
return; |
return; |
||
} |
} |
||
سطر 3٬983: | سطر 3٬333: | ||
var query = { |
var query = { |
||
action: ' |
action: 'pagetriageaction', |
||
pageid: pageID, |
|||
reviewed: 1, |
|||
token: token |
|||
}; |
}; |
||
var triageStat = new Morebits.status('Marking page as curated'); |
|||
ctx.triageProcessListApi.setParent(this); |
|||
ctx.triageProcessListApi.post(); |
|||
}; |
|||
ctx.triageProcessApi = new Morebits.wiki.api('curating page...', query, null, triageStat); |
|||
var fnProcessTriage = function() { |
|||
ctx.triageProcessApi.setParent(this); |
|||
var $xml = $(ctx.triageProcessListApi.getXML()); |
|||
ctx.triageProcessApi.post(); |
|||
// Exit if not in the queue |
|||
if ($xml.find('pagetriagelist').attr('result') !== 'success') { |
|||
return; |
|||
} |
|||
var page = $xml.find('pages _v'); |
|||
// Nothing if page already triaged/patrolled |
|||
if (!page || !parseInt(page.attr('patrol_status'), 10)) { |
|||
var query = { |
|||
action: 'pagetriageaction', |
|||
pageid: ctx.pageID, |
|||
reviewed: 1, |
|||
// tags: ctx.changeTags, // pagetriage tag support: [[phab:T252980]] |
|||
// Could use an adder to modify/create note: |
|||
// summaryAd, but that seems overwrought |
|||
token: ctx.csrfToken |
|||
}; |
|||
var triageStat = new Morebits.status('Marking page as curated'); |
|||
ctx.triageProcessApi = new Morebits.wiki.api('curating page...', query, null, triageStat); |
|||
ctx.triageProcessApi.setParent(this); |
|||
ctx.triageProcessApi.post(); |
|||
} |
|||
}; |
}; |
||
سطر 4٬026: | سطر 3٬355: | ||
var xml = ctx.deleteApi.getXML(); |
var xml = ctx.deleteApi.getXML(); |
||
if ( |
if ($(xml).find('page').attr('missing') === '') { |
||
ctx.statusElement.error('Cannot delete the page, because it no longer exists'); |
|||
return; // abort |
|||
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 ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') + |
|||
'. \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('tokens').attr('csrftoken'); |
token = $(xml).find('tokens').attr('csrftoken'); |
||
if (!token) { |
|||
ctx.statusElement.error('Failed to retrieve delete token.'); |
|||
ctx.onDeleteFailure(this); |
|||
return; |
|||
} |
|||
pageTitle = $(xml).find('page').attr('title'); |
pageTitle = $(xml).find('page').attr('title'); |
||
} |
} |
||
سطر 4٬041: | سطر 3٬389: | ||
'watchlist': ctx.watchlistOption |
'watchlist': ctx.watchlistOption |
||
}; |
}; |
||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
|||
if (ctx.watchlistExpiry) { |
|||
query.watchlistexpiry = ctx.watchlistExpiry; |
|||
} |
|||
ctx.deleteProcessApi = new Morebits.wiki.api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError); |
ctx.deleteProcessApi = new Morebits.wiki.api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError); |
||
سطر 4٬064: | سطر 3٬405: | ||
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
||
ctx.deleteProcessApi.post(); // give it another go! |
ctx.deleteProcessApi.post(); // give it another go! |
||
} else if (errorCode === 'badtoken' && ctx.retries++ < ctx.maxRetries) { |
|||
ctx.statusElement.info('Invalid token, retrying'); |
|||
--Morebits.wiki.numberOfActionsLeft; |
|||
ctx.deleteProcessApi.query.token = fnGetToken.call(this); |
|||
ctx.deleteProcessApi.post(); |
|||
} else if (errorCode === 'missingtitle') { |
} else if (errorCode === 'missingtitle') { |
||
ctx.statusElement.error('Cannot delete the page, because it no longer exists'); |
ctx.statusElement.error('Cannot delete the page, because it no longer exists'); |
||
سطر 4٬088: | سطر 3٬433: | ||
var xml = ctx.undeleteApi.getXML(); |
var xml = ctx.undeleteApi.getXML(); |
||
if ( |
if ($(xml).find('page').attr('missing') !== '') { |
||
ctx.statusElement.error('Cannot undelete the page, because it already exists'); |
|||
return; // abort |
|||
ctx.onUndeleteFailure(this); |
|||
return; |
|||
} |
|||
// extract protection info |
|||
var editprot = $(xml).find('pr[type="create"]'); |
|||
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning && |
|||
!confirm('You are about to undelete the fully create protected page "' + ctx.pageName + |
|||
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') + |
|||
'. \n\nClick OK to proceed with the undeletion, or Cancel to skip this undeletion.')) { |
|||
ctx.statusElement.error('Undeletion of fully create protected page was aborted.'); |
|||
ctx.onUndeleteFailure(this); |
|||
return; |
|||
} |
} |
||
token = $(xml).find('tokens').attr('csrftoken'); |
token = $(xml).find('tokens').attr('csrftoken'); |
||
if (!token) { |
|||
ctx.statusElement.error('Failed to retrieve undelete token.'); |
|||
ctx.onUndeleteFailure(this); |
|||
return; |
|||
} |
|||
pageTitle = $(xml).find('page').attr('title'); |
pageTitle = $(xml).find('page').attr('title'); |
||
} |
} |
||
سطر 4٬103: | سطر 3٬467: | ||
'watchlist': ctx.watchlistOption |
'watchlist': ctx.watchlistOption |
||
}; |
}; |
||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
|||
if (ctx.watchlistExpiry) { |
|||
query.watchlistexpiry = ctx.watchlistExpiry; |
|||
} |
|||
ctx.undeleteProcessApi = new Morebits.wiki.api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError); |
ctx.undeleteProcessApi = new Morebits.wiki.api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError); |
||
سطر 4٬122: | سطر 3٬479: | ||
// check for "Database query error" |
// check for "Database query error" |
||
if (errorCode === 'internal_api_error_DBQueryError') { |
if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) { |
||
ctx.statusElement.info('Database query error, retrying'); |
|||
if (ctx.retries++ < ctx.maxRetries) { |
|||
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
|||
ctx.statusElement.info('Database query error, retrying'); |
|||
ctx.undeleteProcessApi.post(); // give it another go! |
|||
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds |
|||
} else if (errorCode === 'badtoken' && ctx.retries++ < ctx.maxRetries) { |
|||
ctx.undeleteProcessApi.post(); // give it another go! |
|||
ctx.statusElement.info('Invalid token, retrying'); |
|||
} else { |
|||
--Morebits.wiki.numberOfActionsLeft; |
|||
ctx.statusElement.error('Repeated database query error, please try again'); |
|||
ctx.undeleteProcessApi.query.token = fnGetToken.call(this); |
|||
if (ctx.onUndeleteFailure) { |
|||
ctx.undeleteProcessApi.post(); |
|||
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback |
|||
} |
|||
} |
|||
} else if (errorCode === 'cantundelete') { |
} else if (errorCode === 'cantundelete') { |
||
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted'); |
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted'); |
||
سطر 4٬150: | سطر 3٬506: | ||
var xml = ctx.protectApi.getXML(); |
var xml = ctx.protectApi.getXML(); |
||
var missing = $(xml).find('page').attr('missing') === ''; |
|||
if (!fnProcessChecks('protect', ctx.onProtectFailure, xml)) { |
|||
if ((ctx.protectEdit || ctx.protectMove) && missing) { |
|||
return; // abort |
|||
ctx.statusElement.error('Cannot protect the page, because it no longer exists'); |
|||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
|||
if (ctx.protectCreate && !missing) { |
|||
ctx.statusElement.error('Cannot create protect the page, because it already exists'); |
|||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
} |
||
// TODO cascading protection not possible on edit<sysop |
|||
var token = $(xml).find('tokens').attr('csrftoken'); |
var token = $(xml).find('tokens').attr('csrftoken'); |
||
if (!token) { |
|||
ctx.statusElement.error('Failed to retrieve protect token.'); |
|||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
|||
var pageTitle = $(xml).find('page').attr('title'); |
var pageTitle = $(xml).find('page').attr('title'); |
||
// |
// fetch existing protection levels |
||
var prs = $(xml).find('pr'); |
var prs = $(xml).find('pr'); |
||
var editprot = prs.filter('[type="edit"] |
var editprot = prs.filter('[type="edit"]'); |
||
var moveprot = prs.filter('[type="move"]'); |
var moveprot = prs.filter('[type="move"]'); |
||
var createprot = prs.filter('[type="create"]'); |
var createprot = prs.filter('[type="create"]'); |
||
var protections = [], expirys = []; |
|||
// Fall back to current levels if not explicitly set |
|||
if (!ctx.protectEdit && editprot.length) { |
|||
ctx.protectEdit = { level: editprot.attr('level'), expiry: editprot.attr('expiry') }; |
|||
} |
|||
if (!ctx.protectMove && moveprot.length) { |
|||
ctx.protectMove = { level: moveprot.attr('level'), expiry: moveprot.attr('expiry') }; |
|||
} |
|||
if (!ctx.protectCreate && createprot.length) { |
|||
ctx.protectCreate = { level: createprot.attr('level'), expiry: createprot.attr('expiry') }; |
|||
} |
|||
// |
// set edit protection level |
||
// which for edit protection will cause cascading to be silently stripped |
|||
// Also default to pre-existing cascading protection if unchanged (as with others) |
|||
if (ctx.protectCascade || (ctx.protectCascade === null && !!prs.filter('[cascade]').length)) { |
|||
// On move protection, this is technically stricter than the MW API, |
|||
// but seems reasonable to avoid dumb values and misleading log entries (T265626) |
|||
if (((!ctx.protectEdit || ctx.protectEdit.level !== 'sysop') || |
|||
(!ctx.protectMove || ctx.protectMove.level !== 'sysop')) && |
|||
!confirm('You have cascading protection enabled on "' + ctx.pageName + |
|||
'" but have not selected uniform sysop-level protection.\n\n' + |
|||
'Click OK to adjust and proceed with sysop-level cascading protection, or Cancel to skip this action.')) { |
|||
ctx.statusElement.error('Cascading protection was aborted.'); |
|||
ctx.onProtectFailure(this); |
|||
return; |
|||
} |
|||
ctx.protectEdit.level = 'sysop'; |
|||
ctx.protectMove.level = 'sysop'; |
|||
ctx.protectCascade = 'true'; |
|||
} |
|||
// Build protection levels and expirys (expiries?) for query |
|||
var protections = [], expirys = []; |
|||
if (ctx.protectEdit) { |
if (ctx.protectEdit) { |
||
protections.push('edit=' + ctx.protectEdit.level); |
protections.push('edit=' + ctx.protectEdit.level); |
||
expirys.push(ctx.protectEdit.expiry); |
expirys.push(ctx.protectEdit.expiry); |
||
} else if (editprot.length) { |
|||
protections.push('edit=' + editprot.attr('level')); |
|||
expirys.push(editprot.attr('expiry').replace('infinity', 'indefinite')); |
|||
} |
} |
||
سطر 4٬205: | سطر 3٬549: | ||
protections.push('move=' + ctx.protectMove.level); |
protections.push('move=' + ctx.protectMove.level); |
||
expirys.push(ctx.protectMove.expiry); |
expirys.push(ctx.protectMove.expiry); |
||
} else if (moveprot.length) { |
|||
protections.push('move=' + moveprot.attr('level')); |
|||
expirys.push(moveprot.attr('expiry').replace('infinity', 'indefinite')); |
|||
} |
} |
||
سطر 4٬210: | سطر 3٬557: | ||
protections.push('create=' + ctx.protectCreate.level); |
protections.push('create=' + ctx.protectCreate.level); |
||
expirys.push(ctx.protectCreate.expiry); |
expirys.push(ctx.protectCreate.expiry); |
||
} else if (createprot.length) { |
|||
protections.push('create=' + createprot.attr('level')); |
|||
expirys.push(createprot.attr('expiry').replace('infinity', 'indefinite')); |
|||
} |
} |
||
سطر 4٬221: | سطر 3٬571: | ||
watchlist: ctx.watchlistOption |
watchlist: ctx.watchlistOption |
||
}; |
}; |
||
// Only shows up in logs, not page history [[phab:T259983]] |
|||
if (ctx.changeTags) { |
|||
query.tags = ctx.changeTags; |
|||
} |
|||
if (ctx.watchlistExpiry) { |
|||
query.watchlistexpiry = ctx.watchlistExpiry; |
|||
} |
|||
if (ctx.protectCascade) { |
if (ctx.protectCascade) { |
||
query.cascade = 'true'; |
query.cascade = 'true'; |
||
سطر 4٬247: | سطر 3٬589: | ||
var xml = ctx.stabilizeApi.getXML(); |
var xml = ctx.stabilizeApi.getXML(); |
||
var missing = $(xml).find('page').attr('missing') === ''; |
|||
// 'stabilize' as a verb not necessarily well understood |
|||
if (missing) { |
|||
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure, xml)) { |
|||
ctx.statusElement.error('Cannot protect the page, because it no longer exists'); |
|||
return; // abort |
|||
ctx.onStabilizeFailure(this); |
|||
return; |
|||
} |
} |
||
token = $(xml).find('tokens').attr('csrftoken'); |
token = $(xml).find('tokens').attr('csrftoken'); |
||
if (!token) { |
|||
ctx.statusElement.error('Failed to retrieve stabilize token.'); |
|||
ctx.onStabilizeFailure(this); |
|||
return; |
|||
} |
|||
pageTitle = $(xml).find('page').attr('title'); |
pageTitle = $(xml).find('page').attr('title'); |
||
} |
} |
||
سطر 4٬262: | سطر 3٬612: | ||
protectlevel: ctx.flaggedRevs.level, |
protectlevel: ctx.flaggedRevs.level, |
||
expiry: ctx.flaggedRevs.expiry, |
expiry: ctx.flaggedRevs.expiry, |
||
reason: ctx.editSummary |
|||
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]] |
|||
reason: ctx.editSummary, |
|||
watchlist: ctx.watchlistOption // Doesn't support watchlist expiry [[phab:T263336]] |
|||
}; |
}; |
||
// [[phab:T247915]] |
|||
if (ctx.watchlistOption === 'watch') { |
|||
query.watchlist = 'true'; |
|||
} |
|||
ctx.stabilizeProcessApi = new Morebits.wiki.api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure); |
ctx.stabilizeProcessApi = new Morebits.wiki.api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure); |
||
سطر 4٬271: | سطر 3٬623: | ||
ctx.stabilizeProcessApi.post(); |
ctx.stabilizeProcessApi.post(); |
||
}; |
}; |
||
var sleep = function(milliseconds) { |
|||
var deferred = $.Deferred(); |
|||
setTimeout(deferred.resolve, milliseconds); |
|||
return deferred; |
|||
}; |
|||
}; // end Morebits.wiki.page |
}; // end Morebits.wiki.page |
||
سطر 4٬288: | سطر 3٬633: | ||
/* **************** Morebits.wiki.preview **************** */ |
|||
/** |
/** |
||
* **************** Morebits.wiki.preview **************** |
|||
* Use the API to parse a fragment of wikitext and render it as HTML. |
|||
* Uses the API to parse a fragment of wikitext and render it as HTML. |
|||
* |
* |
||
* The suggested implementation pattern (in |
* The suggested implementation pattern (in Morebits.simpleWindow + Morebits.quickForm situations) is to |
||
* construct a Morebits.wiki.preview object after rendering a Morebits.quickForm, and bind the object |
|||
* {@link Morebits.quickForm} situations) is to construct a |
|||
* to an arbitrary property of the form (e.g. |previewer|). For an example, see |
|||
* `Morebits.wiki.preview` object after rendering a `Morebits.quickForm`, and |
|||
* twinklewarn.js. |
|||
* bind the object to an arbitrary property of the form (e.g. |previewer|). |
|||
*/ |
|||
* For an example, see twinklewarn.js. |
|||
* |
|||
/** |
|||
* @memberof Morebits.wiki |
|||
* @ |
* @constructor |
||
* @param {HTMLElement} previewbox - |
* @param {HTMLElement} previewbox - the element that will contain the rendered HTML, |
||
* usually a <div> element |
* usually a <div> element |
||
*/ |
*/ |
||
Morebits.wiki.preview = function(previewbox) { |
Morebits.wiki.preview = function(previewbox) { |
||
سطر 4٬310: | سطر 3٬656: | ||
* Displays the preview box, and begins an asynchronous attempt |
* Displays the preview box, and begins an asynchronous attempt |
||
* to render the specified wikitext. |
* to render the specified wikitext. |
||
* @param {string} wikitext - wikitext to render; most things should work, including subst: and ~~~~ |
|||
* |
|||
* @param {string} |
* @param {string} [pageTitle] - optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page |
||
* @param {string} [pageTitle] - Optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page. |
|||
* @param {string} [sectionTitle] - If provided, render the text as a new section using this as the title. |
|||
*/ |
*/ |
||
this.beginRender = function(wikitext, pageTitle |
this.beginRender = function(wikitext, pageTitle) { |
||
$(previewbox).show(); |
$(previewbox).show(); |
||
سطر 4٬327: | سطر 3٬671: | ||
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: pageTitle || mw.config.get('wgPageName') |
title: pageTitle || mw.config.get('wgPageName') |
||
disablelimitreport: true |
|||
}; |
}; |
||
if (sectionTitle) { |
|||
query.section = 'new'; |
|||
query.sectiontitle = sectionTitle; |
|||
} |
|||
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')); |
||
renderApi.post(); |
renderApi.post(); |
||
سطر 4٬356: | سطر 3٬695: | ||
/* **************** Morebits.wikitext **************** */ |
|||
/** |
/** |
||
* **************** Morebits.wikitext **************** |
|||
* Wikitext manipulation. |
|||
* Wikitext manipulation |
|||
* |
|||
* @namespace Morebits.wikitext |
|||
* @memberof Morebits |
|||
*/ |
*/ |
||
Morebits.wikitext = {}; |
Morebits.wikitext = {}; |
||
Morebits.wikitext.template = { |
|||
/** |
|||
parse: function(text, start) { |
|||
* Get the value of every parameter found in the wikitext of a given template. |
|||
var count = -1; |
|||
* |
|||
var level = -1; |
|||
* @memberof Morebits.wikitext |
|||
var equals = -1; |
|||
* @param {string} text - Wikitext containing a template. |
|||
var current = ''; |
|||
* @param {number} [start=0] - Index noting where in the text the template begins. |
|||
var result = { |
|||
* @returns {object} `{name: templateName, parameters: {key: value}}`. |
|||
name: '', |
|||
*/ |
|||
parameters: {} |
|||
Morebits.wikitext.parseTemplate = function(text, start) { |
|||
}; |
|||
start = start || 0; |
|||
var key, value; |
|||
for (var i = start; i < text.length; ++i) { |
|||
var count = -1; |
|||
var test3 = text.substr(i, 3); |
|||
if (test3 === '{{{') { |
|||
var equals = -1; |
|||
current += '{{{'; |
|||
i += 2; |
|||
var result = { |
|||
++level; |
|||
name: '', |
|||
continue; |
|||
parameters: {} |
|||
} |
|||
}; |
|||
if (test3 === '}}}') { |
|||
var key, value; |
|||
current += '}}}'; |
|||
i += 2; |
|||
--level; |
|||
continue; |
|||
} |
|||
var test2 = text.substr(i, 2); |
|||
if (test2 === '{{' || test2 === '[[') { |
|||
current += test2; |
|||
++i; |
|||
++level; |
|||
continue; |
|||
} |
|||
if (test2 === ']]') { |
|||
current += ']]'; |
|||
++i; |
|||
--level; |
|||
continue; |
|||
} |
|||
if (test2 === '}}') { |
|||
current += test2; |
|||
++i; |
|||
--level; |
|||
if (level <= 0) { |
|||
for (var i = start; i < text.length; ++i) { |
|||
if (count === -1) { |
|||
var test3 = text.substr(i, 3); |
|||
result.name = current.substring(2).trim(); |
|||
if (test3 === '{{{') { |
|||
++count; |
|||
} else { |
|||
if (equals !== -1) { |
|||
++level; |
|||
key = current.substring(0, equals).trim(); |
|||
continue; |
|||
value = current.substring(equals).trim(); |
|||
} |
|||
result.parameters[key] = value; |
|||
if (test3 === '}}}') { |
|||
equals = -1; |
|||
} else { |
|||
result.parameters[count] = current; |
|||
--level; |
|||
++count; |
|||
} |
} |
||
} |
|||
var test2 = text.substr(i, 2); |
|||
break; |
|||
if (test2 === '{{' || test2 === '[[') { |
|||
} |
|||
current += test2; |
|||
continue; |
|||
} |
|||
continue; |
|||
} |
|||
if (test2 === ']]') { |
|||
current += ']]'; |
|||
++i; |
|||
--level; |
|||
continue; |
|||
} |
|||
if (test2 === '}}') { |
|||
current += test2; |
|||
++i; |
|||
--level; |
|||
if (level <= 0) { |
if (text.charAt(i) === '|' && level <= 0) { |
||
if (count === -1) { |
if (count === -1) { |
||
result.name = current.substring(2).trim(); |
result.name = current.substring(2).trim(); |
||
سطر 4٬426: | سطر 3٬774: | ||
if (equals !== -1) { |
if (equals !== -1) { |
||
key = current.substring(0, equals).trim(); |
key = current.substring(0, equals).trim(); |
||
value = current.substring(equals + 1 |
value = current.substring(equals + 1).trim(); |
||
result.parameters[key] = value; |
result.parameters[key] = value; |
||
equals = -1; |
equals = -1; |
||
سطر 4٬434: | سطر 3٬782: | ||
} |
} |
||
} |
} |
||
current = ''; |
|||
} else if (equals === -1 && text.charAt(i) === '=' && level <= 0) { |
|||
equals = current.length; |
|||
current += text.charAt(i); |
|||
} else { |
|||
current += text.charAt(i); |
|||
} |
} |
||
continue; |
|||
} |
} |
||
return result; |
|||
if (text.charAt(i) === '|' && level <= 0) { |
|||
if (count === -1) { |
|||
result.name = current.substring(2).trim(); |
|||
++count; |
|||
} else { |
|||
if (equals !== -1) { |
|||
key = current.substring(0, equals).trim(); |
|||
value = current.substring(equals + 1).trim(); |
|||
result.parameters[key] = value; |
|||
equals = -1; |
|||
} else { |
|||
result.parameters[count] = current; |
|||
++count; |
|||
} |
|||
} |
|||
current = ''; |
|||
} else if (equals === -1 && text.charAt(i) === '=' && level <= 0) { |
|||
equals = current.length; |
|||
current += text.charAt(i); |
|||
} else { |
|||
current += text.charAt(i); |
|||
} |
|||
} |
} |
||
return result; |
|||
}; |
}; |
||
/** |
/** |
||
* @constructor |
|||
* Adjust and manipulate the wikitext of a page. |
|||
* @param {string} text |
|||
* |
|||
* @class |
|||
* @memberof Morebits.wikitext |
|||
* @param {string} text - Wikitext to be manipulated. |
|||
*/ |
*/ |
||
Morebits.wikitext.page = function mediawikiPage(text) { |
Morebits.wikitext.page = function mediawikiPage(text) { |
||
سطر 4٬482: | سطر 3٬808: | ||
/** |
/** |
||
* Removes links to `link_target` from the page text. |
* Removes links to `link_target` from the page text. |
||
* Files and Categories become links with a leading colon |
|||
* (e.g. [[:File:Test.png]]); otherwise, allow for an optional leading |
|||
* colon (e.g. [[:User:Test]]). |
|||
* |
|||
* @param {string} link_target |
* @param {string} link_target |
||
* |
|||
* @returns {Morebits.wikitext.page} |
|||
*/ |
*/ |
||
removeLink: function(link_target) { |
removeLink: function(link_target) { |
||
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() + ']' + |
var link_re_string = '[' + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape(link_target.substr(1), true); |
||
// Files and Categories become links with a leading colon, e.g. [[:File:Test.png]] |
|||
// Otherwise, allow for an optional leading colon, e.g. [[:User:Test]] |
|||
var special_ns_re = /^(?:[Ff]ile|[Ii]mage|[Cc]ategory):/; |
var special_ns_re = /^(?:[Ff]ile|[Ii]mage|[Cc]ategory):/; |
||
var colon = special_ns_re.test(link_target) ? ':' : ':?'; |
var colon = special_ns_re.test(link_target) ? ':' : ':?'; |
||
سطر 4٬500: | سطر 3٬822: | ||
var link_named_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'); |
||
return this; |
|||
}, |
}, |
||
/** |
/** |
||
* Comments out images from page text |
* Comments out images from page text. If used in a gallery, deletes the whole line. |
||
* If used as a template argument (not necessarily with |
* If used as a template argument (not necessarily with File: prefix), the template parameter is commented out. |
||
* @param {string} image - Image name without File: prefix |
|||
* |
|||
* @param {string} |
* @param {string} reason - Reason to be included in comment, alongside the commented-out image |
||
* @param {string} reason - Reason to be included in comment, alongside the commented-out image. |
|||
* |
|||
* @returns {Morebits.wikitext.page} |
|||
*/ |
*/ |
||
commentOutImage: function(image, reason) { |
commentOutImage: function(image, reason) { |
||
سطر 4٬518: | سطر 3٬836: | ||
reason = reason ? reason + ': ' : ''; |
reason = reason ? reason + ': ' : ''; |
||
var first_char = image.substr(0, 1); |
var first_char = image.substr(0, 1); |
||
var image_re_string = '[' + first_char.toUpperCase() + first_char.toLowerCase() + ']' + |
var image_re_string = '[' + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape(image.substr(1), true); |
||
// Check for normal image links, i.e. [[File:Foobar.png|...]] |
// Check for normal image links, i.e. [[File:Foobar.png|...]] |
||
سطر 4٬548: | سطر 3٬866: | ||
// Rebind the content now, we are done! |
// Rebind the content now, we are done! |
||
this.text = unbinder.rebind(); |
this.text = unbinder.rebind(); |
||
return this; |
|||
}, |
}, |
||
/** |
/** |
||
* Converts |
* Converts first usage of [[File:`image`]] to [[File:`image`|`data`]] |
||
* @param {string} image - Image name without File: prefix |
|||
* |
|||
* @param {string} |
* @param {string} data |
||
* @param {string} data - The display options. |
|||
* |
|||
* @returns {Morebits.wikitext.page} |
|||
*/ |
*/ |
||
addToImageComment: function(image, data) { |
addToImageComment: function(image, data) { |
||
var first_char = image.substr(0, 1); |
var first_char = image.substr(0, 1); |
||
var first_char_regex = |
var first_char_regex = RegExp.escape(first_char, true); |
||
if (first_char.toUpperCase() !== first_char.toLowerCase()) { |
if (first_char.toUpperCase() !== first_char.toLowerCase()) { |
||
first_char_regex = '[' + |
first_char_regex = '[' + RegExp.escape(first_char.toUpperCase(), true) + RegExp.escape(first_char.toLowerCase(), true) + ']'; |
||
} |
} |
||
var image_re_string = '(?:[Ii]mage|[Ff]ile):\\s*' + first_char_regex + |
var image_re_string = '(?:[Ii]mage|[Ff]ile):\\s*' + first_char_regex + RegExp.escape(image.substr(1), true); |
||
var links_re = new RegExp('\\[\\[' + image_re_string); |
var links_re = new RegExp('\\[\\[' + image_re_string); |
||
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys(this.text, '[[', ']]')); |
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys(this.text, '[[', ']]')); |
||
سطر 4٬579: | سطر 3٬893: | ||
var newtext = '$1|$2 ' + data; |
var newtext = '$1|$2 ' + data; |
||
this.text = this.text.replace(gallery_re, newtext); |
this.text = this.text.replace(gallery_re, newtext); |
||
return this; |
|||
}, |
}, |
||
/** |
/** |
||
* Removes transclusions of template from page text |
* Removes transclusions of template from page text |
||
* |
|||
* @param {string} template - Page name whose transclusions are to be removed, |
* @param {string} template - Page name whose transclusions are to be removed, |
||
* include namespace prefix only if not in template namespace |
* include namespace prefix only if not in template namespace |
||
* |
|||
* @returns {Morebits.wikitext.page} |
|||
*/ |
*/ |
||
removeTemplate: function(template) { |
removeTemplate: function(template) { |
||
var first_char = template.substr(0, 1); |
var first_char = template.substr(0, 1); |
||
var template_re_string = '(?:[Tt]emplate:)?\\s*[' + first_char.toUpperCase() + first_char.toLowerCase() + ']' + |
var template_re_string = '(?:[Tt]emplate:)?\\s*[' + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape(template.substr(1), true); |
||
var links_re = new RegExp('\\{\\{' + template_re_string); |
var links_re = new RegExp('\\{\\{' + template_re_string); |
||
var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', [ '{{{', '}}}' ])); |
var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', [ '{{{', '}}}' ])); |
||
سطر 4٬600: | سطر 3٬910: | ||
} |
} |
||
} |
} |
||
return this; |
|||
}, |
}, |
||
/** @returns {string} */ |
|||
/** |
|||
* Smartly insert a tag atop page text but after specified templates, |
|||
* such as hatnotes, short description, or deletion and protection templates. |
|||
* Notably, does *not* insert a newline after the tag. |
|||
* |
|||
* @param {string} tag - The tag to be inserted. |
|||
* @param {string|string[]} regex - Templates after which to insert tag, |
|||
* given as either as a (regex-valid) string or an array to be joined by pipes. |
|||
* @param {string} [flags=i] - Regex flags to apply. |
|||
* @param {string|string[]} preRegex - Optional regex string or array to match |
|||
* before any template matches (i.e. before `{{`), such as html comments. |
|||
* @returns {Morebits.wikitext.page} |
|||
* @throws If no tag or regex are provided. |
|||
*/ |
|||
insertAfterTemplates: function(tag, regex, flags, preRegex) { |
|||
if (typeof tag === 'undefined') { |
|||
throw new Error('No tag provided'); |
|||
} |
|||
// .length is only a property of strings and arrays so we |
|||
// shouldn't need to check type |
|||
if (typeof regex === 'undefined' || !regex.length) { |
|||
throw new Error('No regex provided'); |
|||
} else if (Array.isArray(regex)) { |
|||
regex = regex.join('|'); |
|||
} |
|||
flags = flags || 'i'; |
|||
if (!preRegex || !preRegex.length) { |
|||
preRegex = ''; |
|||
} else if (Array.isArray(preRegex)) { |
|||
preRegex = preRegex.join('|'); |
|||
} |
|||
// Regex is extra complicated to allow for templates with |
|||
// parameters and to handle whitespace properly |
|||
this.text = this.text.replace( |
|||
new RegExp( |
|||
// leading whitespace |
|||
'^\\s*' + |
|||
// capture template(s) |
|||
'(?:((?:\\s*' + |
|||
// Pre-template regex, such as leading html comments |
|||
preRegex + '|' + |
|||
// begin template format |
|||
'\\{\\{\\s*(?:' + |
|||
// Template regex |
|||
regex + |
|||
// end main template name, optionally with a number |
|||
// Probably remove the (?:) though |
|||
')\\d*\\s*' + |
|||
// template parameters |
|||
'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' + |
|||
// end template format |
|||
'\\}\\})+' + |
|||
// end capture |
|||
'(?:\\s*\\n)?)' + |
|||
// trailing whitespace |
|||
'\\s*)?', |
|||
flags), '$1' + tag |
|||
); |
|||
return this; |
|||
}, |
|||
/** |
|||
* Get the manipulated wikitext. |
|||
* |
|||
* @returns {string} |
|||
*/ |
|||
getText: function() { |
getText: function() { |
||
return this.text; |
return this.text; |
||
سطر 4٬679: | سطر 3٬918: | ||
}; |
}; |
||
/* *********** Morebits.userspaceLogger ************ */ |
|||
/** |
/** |
||
* **************** Morebits.status **************** |
|||
* Handles logging actions to a userspace log. |
|||
* Used in CSD, PROD, and XFD. |
|||
* |
|||
* @memberof Morebits |
|||
* @class |
|||
* @param {string} logPageName - Title of the subpage of the current user's log. |
|||
*/ |
*/ |
||
Morebits.userspaceLogger = function(logPageName) { |
|||
if (!logPageName) { |
|||
throw new Error('no log page name specified'); |
|||
} |
|||
/** |
|||
* The text to prefix the log with upon creation, defaults to empty. |
|||
* |
|||
* @type {string} |
|||
*/ |
|||
this.initialText = ''; |
|||
/** |
|||
* The header level to use for months, defaults to 3 (`===`). |
|||
* |
|||
* @type {number} |
|||
*/ |
|||
this.headerLevel = 3; |
|||
this.changeTags = ''; |
|||
/** |
|||
* Log the entry. |
|||
* |
|||
* @param {string} logText - Doesn't include leading `#` or `*`. |
|||
* @param {string} summaryText - Edit summary. |
|||
*/ |
|||
this.log = function(logText, summaryText) { |
|||
if (!logText) { |
|||
return; |
|||
} |
|||
var page = new Morebits.wiki.page('User:' + mw.config.get('wgUserName') + '/' + logPageName, |
|||
'Adding entry to userspace log'); // make this '... to ' + logPageName ? |
|||
return page.load(function(pageobj) { |
|||
// add blurb if log page doesn't exist or is blank |
|||
var text = pageobj.getPageText() || this.initialText; |
|||
// create monthly header if it doesn't exist already |
|||
var date = new Morebits.date(pageobj.getLoadTime()); |
|||
if (!date.monthHeaderRegex().exec(text)) { |
|||
text += '\n\n' + date.monthHeader(this.headerLevel); |
|||
} |
|||
pageobj.setPageText(text + '\n' + logText); |
|||
pageobj.setEditSummary(summaryText); |
|||
pageobj.setChangeTags(this.changeTags); |
|||
pageobj.setCreateOption('recreate'); |
|||
pageobj.save(); |
|||
}.bind(this)); |
|||
}; |
|||
}; |
|||
/* **************** Morebits.status **************** */ |
|||
/** |
/** |
||
* @constructor |
|||
* Create and show status messages of varying urgency. |
|||
* |
* Morebits.status.init() must be called before any status object is created, otherwise |
||
* |
* those statuses won't be visible. |
||
* @param {String} text - Text before the the colon `:` |
|||
* |
|||
* @param {String} stat - Text after the colon `:` |
|||
* @memberof Morebits |
|||
* @param {String} [type=status] - This parameter determines the font color of the status line, |
|||
* @class |
|||
* this can be 'status' (blue), 'info' (green), 'warn' (red), or 'error' (bold red) |
|||
* @param {string} text - Text before the the colon `:`. |
|||
* The default is 'status' |
|||
* @param {string} stat - Text after the colon `:`. |
|||
* @param {string} [type=status] - Determine the font color of the status |
|||
* line, allowable values are: `status` (blue), `info` (green), `warn` (red), |
|||
* or `error` (bold red). |
|||
*/ |
*/ |
||
سطر 4٬765: | سطر 3٬944: | ||
/** |
/** |
||
* Specify an area for status message elements to be added to |
* Specify an area for status message elements to be added to |
||
* @param {HTMLElement} root - usually a div element |
|||
* |
|||
* @memberof Morebits.status |
|||
* @param {HTMLElement} root - Usually a div element. |
|||
* @throws If `root` is not an `HTMLElement`. |
|||
*/ |
*/ |
||
Morebits.status.init = function(root) { |
Morebits.status.init = function(root) { |
||
سطر 4٬784: | سطر 3٬960: | ||
Morebits.status.root = null; |
Morebits.status.root = null; |
||
/** @param {Function} handler - function to execute on error */ |
|||
/** |
|||
* @memberof Morebits.status |
|||
* @param {Function} handler - Function to execute on error. |
|||
* @throws When `handler` is not a function. |
|||
*/ |
|||
Morebits.status.onError = function(handler) { |
Morebits.status.onError = function(handler) { |
||
if (typeof handler === 'function') { |
if (typeof handler === 'function') { |
||
سطر 4٬799: | سطر 3٬971: | ||
Morebits.status.prototype = { |
Morebits.status.prototype = { |
||
stat: null, |
stat: null, |
||
statRaw: null, |
|||
text: null, |
text: null, |
||
textRaw: null, |
textRaw: null, |
||
سطر 4٬807: | سطر 3٬978: | ||
linked: false, |
linked: false, |
||
/** Add the status element node to the DOM |
/** Add the status element node to the DOM */ |
||
link: function() { |
link: function() { |
||
if (!this.linked && Morebits.status.root) { |
if (!this.linked && Morebits.status.root) { |
||
سطر 4٬815: | سطر 3٬986: | ||
}, |
}, |
||
/** Remove the status element node from the DOM |
/** Remove the status element node from the DOM */ |
||
unlink: function() { |
unlink: function() { |
||
if (this.linked) { |
if (this.linked) { |
||
سطر 4٬824: | سطر 3٬995: | ||
/** |
/** |
||
* Create a document fragment with the status text |
* Create a document fragment with the status text |
||
* Runs upon construction for text (part before colon) and upon |
|||
* render/update for status (part after colon). |
|||
* |
|||
* @param {(string|Element|Array)} obj |
* @param {(string|Element|Array)} obj |
||
* @returns {DocumentFragment} |
* @returns {DocumentFragment} |
||
سطر 4٬838: | سطر 4٬006: | ||
result = document.createDocumentFragment(); |
result = document.createDocumentFragment(); |
||
for (var i = 0; i < obj.length; ++i) { |
for (var i = 0; i < obj.length; ++i) { |
||
if (obj[i] |
if (typeof obj[i] === 'string') { |
||
result.appendChild(document.createTextNode(obj[i])); |
|||
} else if (obj[i] instanceof Element) { |
|||
result.appendChild(obj[i]); |
result.appendChild(obj[i]); |
||
} // Else cosmic radiation made something shit |
|||
} else { |
|||
$.parseHTML(obj[i]).forEach(function(elem) { |
|||
result.appendChild(elem); |
|||
}); |
|||
} |
|||
} |
} |
||
return result; |
return result; |
||
سطر 4٬851: | سطر 4٬017: | ||
/** |
/** |
||
* Update the status |
* Update the status |
||
* @param {String} status - Part of status message after colon `:` |
|||
* |
|||
* @param { |
* @param {String} type - 'status' (blue), 'info' (green), 'warn' (red), or 'error' (bold red) |
||
* @param {string} type - 'status' (blue), 'info' (green), 'warn' |
|||
* (red), or 'error' (bold red). FIXME TODO possible options |
|||
*/ |
*/ |
||
update: function(status, type) { |
update: function(status, type) { |
||
this.statRaw = status; |
|||
this.stat = this.codify(status); |
this.stat = this.codify(status); |
||
if (type) { |
if (type) { |
||
سطر 4٬872: | سطر 4٬035: | ||
// also log error messages in the browser console |
// also log error messages in the browser console |
||
console.error(this.textRaw + ': ' + |
console.error(this.textRaw + ': ' + status); // eslint-disable-line no-console |
||
} |
} |
||
} |
} |
||
سطر 4٬878: | سطر 4٬041: | ||
}, |
}, |
||
/** Produce the html for first part of the status message |
/** Produce the html for first part of the status message */ |
||
generate: function() { |
generate: function() { |
||
this.node = document.createElement('div'); |
this.node = document.createElement('div'); |
||
سطر 4٬887: | سطر 4٬050: | ||
}, |
}, |
||
/** Complete the html, for the second part of the status message |
/** Complete the html, for the second part of the status message */ |
||
render: function() { |
render: function() { |
||
this.node.className = 'morebits_status_' + this.type; |
this.node.className = 'morebits_status_' + this.type; |
||
سطر 4٬909: | سطر 4٬072: | ||
} |
} |
||
}; |
}; |
||
Morebits.status.info = function(text, status) { |
Morebits.status.info = function(text, status) { |
||
return new Morebits.status(text, status, 'info'); |
return new Morebits.status(text, status, 'info'); |
||
سطر 4٬924: | سطر 4٬088: | ||
* For the action complete message at the end, create a status line without |
* For the action complete message at the end, create a status line without |
||
* a colon separator. |
* a colon separator. |
||
* @param {String} text |
|||
* |
|||
* @memberof Morebits.status |
|||
* @param {string} text |
|||
*/ |
*/ |
||
Morebits.status.actionCompleted = function(text) { |
Morebits.status.actionCompleted = function(text) { |
||
var node = document.createElement('div'); |
var node = document.createElement('div'); |
||
node.appendChild(document.createElement(' |
node.appendChild(document.createElement('span')).appendChild(document.createTextNode(text)); |
||
node.className = 'morebits_status_info'; |
node.className = 'morebits_status_info'; |
||
if (Morebits.status.root) { |
if (Morebits.status.root) { |
||
سطر 4٬938: | سطر 4٬100: | ||
/** |
/** |
||
* Display the user's rationale, comments, etc. |
* Display the user's rationale, comments, etc. back to them after a failure, |
||
* so that they may re-use it |
* so that they may re-use it |
||
* |
|||
* @memberof Morebits.status |
|||
* @param {string} comments |
* @param {string} comments |
||
* @param {string} message |
* @param {string} message |
||
سطر 4٬960: | سطر 4٬120: | ||
/** |
/** |
||
* **************** Morebits.htmlNode() **************** |
|||
* Simple helper function to create a simple node. |
|||
* Simple helper function to create a simple node |
|||
* |
|||
* @param {string} type - |
* @param {string} type - type of HTML element |
||
* @param {string} |
* @param {string} text - text content |
||
* @param {string} [color] - |
* @param {string} [color] - font color |
||
* @returns {HTMLElement} |
* @returns {HTMLElement} |
||
*/ |
*/ |
||
سطر 4٬979: | سطر 4٬139: | ||
/** |
/** |
||
* **************** Morebits.checkboxShiftClickSupport() **************** |
|||
* Add shift-click support for checkboxes. The wikibits version |
|||
* shift-click-support for checkboxes |
|||
* (`window.addCheckboxClickHandlers`) has some restrictions, and doesn't work |
|||
* wikibits version (window.addCheckboxClickHandlers) has some restrictions, and |
|||
* with checkboxes inside a sortable table, so let's build our own. |
|||
* doesn't work with checkboxes inside a sortable table, so let's build our own. |
|||
* |
|||
* @param jQuerySelector |
|||
* @param jQueryContext |
|||
*/ |
*/ |
||
Morebits.checkboxShiftClickSupport = function (jQuerySelector, jQueryContext) { |
Morebits.checkboxShiftClickSupport = function (jQuerySelector, jQueryContext) { |
||
سطر 5٬037: | سطر 4٬195: | ||
/* **************** Morebits.batchOperation **************** |
/** **************** Morebits.batchOperation **************** |
||
/** |
|||
* Iterates over a group of pages (or arbitrary objects) and executes a worker function |
* Iterates over a group of pages (or arbitrary objects) and executes a worker function |
||
* for each. |
* for each. |
||
* |
* |
||
* Constructor: Morebits.batchOperation(currentAction) |
|||
* `setPageList(pageList)`: Sets the list of pages to work on. It should be an |
|||
* |
|||
* array of page names strings. |
|||
* setPageList(wikitext): Sets the list of pages to work on. |
|||
* It should be an array of page names (strings). |
|||
* |
* |
||
* |
* setOption(optionName, optionValue): Sets a known option: |
||
* - |
* - chunkSize (integer): the size of chunks to break the array into (default 50). |
||
* |
* Setting this to a small value (<5) can cause problems. |
||
* - |
* - preserveIndividualStatusLines (boolean): keep each page's status element visible |
||
* |
* when worker is complete? See note below |
||
* |
* |
||
* |
* run(worker, postFinish): Runs the callback `worker` for each page in the list. |
||
* |
* The callback must call workerSuccess when succeeding, or workerFailure |
||
* |
* 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 |
||
* |
* objects, for instance, page.save(batchOp.workerSuccess, batchOp.workerFailure). |
||
* Make sure the methods are called directly if special success/failure cases arise. |
|||
* `page.save(batchOp.workerSuccess, batchOp.workerFailure)`. Make sure the |
|||
* If you omit to call these methods, the batch operation will stall after the first |
|||
* methods are called directly if special success/failure cases arise. If you |
|||
* chunk! Also ensure that either workerSuccess or workerFailure is called no more |
|||
* omit to call these methods, the batch operation will stall after the first |
|||
* than once. |
|||
* chunk! Also ensure that either workerSuccess or workerFailure is called no |
|||
* |
* The second callback `postFinish` is executed when the entire batch has been processed. |
||
* entire batch has been processed. |
|||
* |
* |
||
* If using |
* If using preserveIndividualStatusLines, you should try to ensure that the |
||
* |
* 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 |
||
* |pageName| property on the |
* |pageName| property on the Morebits.wiki.api object. |
||
* |
* |
||
* There are sample batchOperation implementations using Morebits.wiki.page in |
* There are sample batchOperation implementations using Morebits.wiki.page in |
||
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js. |
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js. |
||
* |
*/ |
||
* @memberof Morebits |
|||
/** |
|||
* @class |
|||
* @constructor |
|||
* @param {string} [currentAction] |
* @param {string} [currentAction] |
||
*/ |
*/ |
||
سطر 5٬102: | سطر 4٬261: | ||
/** |
/** |
||
* Sets the list of pages to work on |
* Sets the list of pages to work on |
||
* @param {Array} pageList Array of objects over which you wish to execute the worker function |
|||
* |
|||
* @param {Array} pageList - Array of objects over which you wish to execute the worker function |
|||
* This is usually the list of page names (strings). |
* This is usually the list of page names (strings). |
||
*/ |
*/ |
||
سطر 5٬112: | سطر 4٬270: | ||
/** |
/** |
||
* Sets a known option |
* Sets a known option: |
||
* - chunkSize (integer): |
|||
* |
|||
* The size of chunks to break the array into (default 50). |
|||
* @param {string} optionName - Name of the option: |
|||
* Setting this to a small value (<5) can cause problems. |
|||
* - chunkSize (integer): The size of chunks to break the array into |
|||
* - preserveIndividualStatusLines (boolean): |
|||
* (default 50). Setting this to a small value (<5) can cause problems. |
|||
* |
* Keep each page's status element visible when worker is complete? |
||
* element visible when worker is complete? |
|||
* @param {number|boolean} optionValue - Value to which the option is |
|||
* to be set. Should be an integer for chunkSize and a boolean for |
|||
* preserveIndividualStatusLines. |
|||
*/ |
*/ |
||
this.setOption = function(optionName, optionValue) { |
this.setOption = function(optionName, optionValue) { |
||
سطر 5٬130: | سطر 4٬284: | ||
* Runs the first callback for each page in the list. |
* Runs the first callback for each page in the list. |
||
* The callback must call workerSuccess when succeeding, or workerFailure when failing. |
* The callback must call workerSuccess when succeeding, or workerFailure when failing. |
||
* Runs the |
* Runs the second callback when the whole batch has been processed (optional) |
||
* |
|||
* @param {Function} worker |
* @param {Function} worker |
||
* @param {Function} [postFinish] |
* @param {Function} [postFinish] |
||
سطر 5٬170: | سطر 4٬323: | ||
/** |
/** |
||
* To be called by worker before it terminates succesfully |
* To be called by worker before it terminates succesfully |
||
* @param {(Morebits.wiki.page|Morebits.wiki.api|string)} arg |
|||
* |
|||
* @param {(Morebits.wiki.page|Morebits.wiki.api|string)} arg - |
|||
* This should be the `Morebits.wiki.page` or `Morebits.wiki.api` object used by worker |
* This should be the `Morebits.wiki.page` or `Morebits.wiki.api` object used by worker |
||
* (for the adjustment of status lines emitted by them). |
* (for the adjustment of status lines emitted by them). |
||
* If no Morebits.wiki.* object is used ( |
* If no Morebits.wiki.* object is used (eg. you're using mw.Api() or something else), and |
||
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument. |
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument. |
||
*/ |
*/ |
||
سطر 5٬238: | سطر 4٬390: | ||
// update overall status line |
// update overall status line |
||
var total = ctx.pageList.length; |
var total = ctx.pageList.length; |
||
if (ctx.countFinished |
if (ctx.countFinished === total) { |
||
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(); |
|||
} |
|||
} else if (ctx.countFinished === total) { |
|||
var statusString = 'Done (' + ctx.countFinishedSuccess + |
var statusString = 'Done (' + ctx.countFinishedSuccess + |
||
'/' + ctx.countFinished + ' actions completed successfully)'; |
'/' + ctx.countFinished + ' actions completed successfully)'; |
||
سطر 5٬260: | سطر 4٬403: | ||
Morebits.wiki.removeCheckpoint(); |
Morebits.wiki.removeCheckpoint(); |
||
ctx.running = false; |
ctx.running = false; |
||
return; |
|||
} else { |
|||
} |
|||
// ctx.countFinished > total |
|||
// just for giggles! (well, serious debugging, actually) |
|||
// just for giggles! (well, serious debugging, actually) |
|||
if (ctx.countFinished > total) { |
|||
ctx.statusElement.warn('Done (overshot by ' + (ctx.countFinished - total) + ')'); |
ctx.statusElement.warn('Done (overshot by ' + (ctx.countFinished - total) + ')'); |
||
Morebits.wiki.removeCheckpoint(); |
Morebits.wiki.removeCheckpoint(); |
||
ctx.running = false; |
ctx.running = false; |
||
return; |
|||
} |
} |
||
}; |
|||
}; |
|||
ctx.statusElement.status(parseInt(100 * ctx.countFinished / total, 10) + '%'); |
|||
/** |
|||
* Given a set of asynchronous functions to run along with their dependencies, |
|||
* figure out an efficient sequence of running them so that multiple functions |
|||
* that don't depend on each other are triggered simultaneously. Where |
|||
* dependencies exist, it ensures that the dependency functions finish running |
|||
* before the dependent function runs. The values resolved by the dependencies |
|||
* are made available to the dependant as arguments. |
|||
* |
|||
* @memberof Morebits |
|||
* @class |
|||
*/ |
|||
Morebits.taskManager = function() { |
|||
this.taskDependencyMap = new Map(); |
|||
this.deferreds = new Map(); |
|||
this.allDeferreds = []; // Hack: IE doesn't support Map.prototype.values |
|||
// 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 |
|||
* Register a task along with its dependencies (tasks which should have finished |
|||
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) && |
|||
* execution before we can begin this one). Each task is a function that must return |
|||
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) { |
|||
* a promise. The function will get the values resolved by the dependency functions |
|||
fnStartNewChunk(); |
|||
* as arguments. |
|||
} |
|||
* @param {Function} func - A task. |
|||
* @param {Function[]} deps - Its dependencies. |
|||
*/ |
|||
this.add = function(func, deps) { |
|||
this.taskDependencyMap.set(func, deps); |
|||
var deferred = $.Deferred(); |
|||
this.deferreds.set(func, deferred); |
|||
this.allDeferreds.push(deferred); |
|||
}; |
}; |
||
}; |
|||
/** |
|||
* Run all the tasks. Multiple tasks may be run at once. |
|||
* |
|||
* @returns {promise} - A jQuery promise object that is resolved or rejected with the api object. |
|||
*/ |
|||
this.execute = function() { |
|||
var self = this; // proxy for `this` for use inside functions where `this` is something else |
|||
this.taskDependencyMap.forEach(function(deps, task) { |
|||
var dependencyPromisesArray = deps.map(function(dep) { |
|||
return self.deferreds.get(dep); |
|||
}); |
|||
$.when.apply(null, dependencyPromisesArray).then(function() { |
|||
task.apply(null, arguments).then(function() { |
|||
self.deferreds.get(task).resolve.apply(null, arguments); |
|||
}); |
|||
}); |
|||
}); |
|||
return $.when.apply(null, this.allDeferreds); // resolved when everything is done! |
|||
}; |
|||
}; |
|||
/** |
/** |
||
* **************** Morebits.simpleWindow **************** |
|||
* A simple draggable window, now a wrapper for jQuery UI's dialog feature. |
|||
* A simple draggable window |
|||
* |
|||
* now a wrapper for jQuery UI's dialog feature |
|||
* @memberof Morebits |
|||
* @requires {jquery.ui.dialog} |
|||
* @class |
|||
*/ |
|||
* @requires jquery.ui |
|||
/** |
|||
* @constructor |
|||
* @param {number} width |
* @param {number} width |
||
* @param {number} height |
* @param {number} height The maximum allowable height for the content area. |
||
*/ |
*/ |
||
Morebits.simpleWindow = function SimpleWindow(width, height) { |
Morebits.simpleWindow = function SimpleWindow(width, height) { |
||
سطر 5٬373: | سطر 4٬479: | ||
var $widget = $(this.content).dialog('widget'); |
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) |
// delete the placeholder button (it's only there so the buttonpane gets created) |
||
سطر 5٬398: | سطر 4٬509: | ||
/** |
/** |
||
* Focuses the dialog. This might work, or on the contrary, it might not. |
* Focuses the dialog. This might work, or on the contrary, it might not. |
||
* |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬409: | سطر 4٬519: | ||
* Closes the dialog. If this is set as an event handler, it will stop the event |
* Closes the dialog. If this is set as an event handler, it will stop the event |
||
* from doing anything more. |
* from doing anything more. |
||
* |
|||
* @param {event} [event] |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬424: | سطر 4٬532: | ||
* Shows the dialog. Calling display() on a dialog that has previously been closed |
* Shows the dialog. Calling display() on a dialog that has previously been closed |
||
* might work, but it is not guaranteed. |
* might work, but it is not guaranteed. |
||
* |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬448: | سطر 4٬555: | ||
/** |
/** |
||
* Sets the dialog title. |
* Sets the dialog title. |
||
* |
|||
* @param {string} title |
* @param {string} title |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬460: | سطر 4٬566: | ||
* Sets the script name, appearing as a prefix to the title to help users determine which |
* Sets the script name, appearing as a prefix to the title to help users determine which |
||
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle". |
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle". |
||
* |
|||
* @param {string} name |
* @param {string} name |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬471: | سطر 4٬576: | ||
/** |
/** |
||
* Sets the dialog width. |
* Sets the dialog width. |
||
* |
|||
* @param {number} width |
* @param {number} width |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬483: | سطر 4٬587: | ||
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents, |
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents, |
||
* but the content area will grow no larger than the height given here. |
* but the content area will grow no larger than the height given here. |
||
* |
|||
* @param {number} height |
* @param {number} height |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬506: | سطر 4٬609: | ||
/** |
/** |
||
* Sets the content of the dialog to the given element node, usually from rendering |
* Sets the content of the dialog to the given element node, usually from rendering |
||
* a |
* a Morebits.quickForm. |
||
* Re-enumerates the footer buttons, but leaves the footer links as they are. |
* Re-enumerates the footer buttons, but leaves the footer links as they are. |
||
* Be sure to call this at least once before the dialog is displayed... |
* Be sure to call this at least once before the dialog is displayed... |
||
* |
|||
* @param {HTMLElement} content |
* @param {HTMLElement} content |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬521: | سطر 4٬623: | ||
/** |
/** |
||
* Adds the given element node to the dialog content. |
* Adds the given element node to the dialog content. |
||
* |
|||
* @param {HTMLElement} content |
* @param {HTMLElement} content |
||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
سطر 5٬551: | سطر 4٬652: | ||
/** |
/** |
||
* Removes all contents from the dialog, barring any footer links |
* Removes all contents from the dialog, barring any footer links |
||
* |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬571: | سطر 4٬671: | ||
* For example, Twinkle's CSD module adds a link to the CSD policy page, |
* For example, Twinkle's CSD module adds a link to the CSD policy page, |
||
* as well as a link to Twinkle's documentation. |
* as well as a link to Twinkle's documentation. |
||
* @param {string} text Link's text content |
|||
* |
|||
* @param {string} |
* @param {string} wikiPage Link target |
||
* @param { |
* @param {boolean} [prep=false] Set true to prepend rather than append |
||
* @param {boolean} [prep=false] - Set true to prepend rather than append. |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬603: | سطر 4٬702: | ||
/** |
/** |
||
* |
* Set whether the window should be modal or not. |
||
* |
* If set to true, other items on the page will be disabled, i.e., cannot be |
||
* interacted with. Modal dialogs create an overlay below the dialog but above |
|||
* must be used (if necessary) before calling display(). |
|||
* other page elements. |
|||
* |
|||
* This must be used (if necessary) before calling display() |
|||
* @param {boolean} [modal=false] - If set to true, other items on the |
|||
* Default: false |
|||
* page will be disabled, i.e., cannot be interacted with. |
|||
* @param {boolean} modal |
|||
* @returns {Morebits.simpleWindow} |
* @returns {Morebits.simpleWindow} |
||
*/ |
*/ |
||
سطر 5٬618: | سطر 4٬718: | ||
/** |
/** |
||
* Enables or disables all footer buttons on all |
* Enables or disables all footer buttons on all Morebits.simpleWindows in the current page. |
||
* This should be called with `false` when the button(s) become irrelevant (e.g. just before |
* This should be called with `false` when the button(s) become irrelevant (e.g. just before |
||
* |
* Morebits.status.init is called). |
||
* This is not an instance method so that consumers don't have to keep a reference to the |
* This is not an instance method so that consumers don't have to keep a reference to the |
||
* original |
* original Morebits.simpleWindow object sitting around somewhere. Anyway, most of the time |
||
* there will only be one |
* there will only be one Morebits.simpleWindow open, so this shouldn't matter. |
||
* |
|||
* @memberof Morebits.simpleWindow |
|||
* @param {boolean} enabled |
* @param {boolean} enabled |
||
*/ |
*/ |
||
سطر 5٬631: | سطر 4٬729: | ||
$('.morebits-dialog-buttons button').prop('disabled', !enabled); |
$('.morebits-dialog-buttons button').prop('disabled', !enabled); |
||
}; |
}; |
||
// Twinkle blacklist was removed per consensus at http://en.wikipedia.org/wiki/Wikipedia:Administrators%27_noticeboard/Archive221#New_Twinkle_blacklist_proposal |
|||
سطر 5٬638: | سطر 4٬740: | ||
/** |
/** |
||
* If this script is being executed outside a ResourceLoader context, we add some |
* If this script is being executed outside a ResourceLoader context, we add some |
||
* global assignments for legacy scripts, hopefully these can be removed down the line |
* global assignments for legacy scripts, hopefully these can be removed down the line |
||
* |
* |
||
* IMPORTANT NOTE: |
* IMPORTANT NOTE: |
||
سطر 5٬654: | سطر 4٬756: | ||
// </nowiki> |
// </nowiki> |
||
</syntaxhighlight> |