User:DeletedUser20739583/global.js

importArticles({   type: 'script',    articles: [    	'u:dev:MediaWiki:MassNullEdit/code.js',    	'u:dev:MediaWiki:Rollback/code.js',        'u:dev:MediaWiki:MassCategorization/code.js'    ] });

require(["jquery", "mw", "wikia.window", "wikia.nirvana"],   function ($, mw, wk, nv) {  "use strict";  // Define extant global object config if needed  wk.dev = wk.dev || {};  wk.dev.massEdit = wk.dev.massEdit || {};  // Prevent double loads and respect prior double load check formatting  if (wk.dev.massEdit.isLoaded || wk.isMassEditLoaded) {    return;  }  wk.dev.massEdit.isLoaded = true;  /**   * @description The   namespace object is used as a class   * prototype for the MassEdit class instance. It contains methods and   * properties related to the actual MassEdit functionality and application   * logic, keeping in a separate object all the methods used to initialize the   * script itself.   *   * @const   */  const main = {};  /**   * @description The   namespace object contains methods and   * properties related to the initialization of the MassEdit script. The   * methods in this namespace object are responsible for loading external * dependencies, validating user input, setting config, and creating a new * MassEdit instance once setup is complete. *  * @const */ const init = {}; /**  * @description This simple   flag is used to log messages * in the console at sensitive application logic problem areas where issues * are known to arise. Originally, DEBUG was part of an enum alongside a unit * testing ; however, the removal of unit tests at the end * of the testing period returned DEBUG to the script-global scope. *  * @const */ const DEBUG = false; /****************************************************************************/ /*                         Prototype pseudo-enums                           */ /****************************************************************************/ // Protected pseudo-enums of prototype Object.defineProperties(main, {   /**     * @description This pseudo-enum of the   namespace object     * is used to store all CSS selectors in a single place in the event that     * one or more need to be changed. The formatting of the object literal key     * naming is type (id or class), location (placement, modal, content, * preview), and either the name for ids or the type of element (div, span, * etc.). Originally, these were all divided into nested object literals as    * seen in Message.js. However, this system became too unreadable in the     * body of the script, necessitating a simpler system.     *     * @readonly     * @enum {string}     */    Selectors: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ // Toolbar placement ids ID_PLACEMENT_LIST: "massedit-placement-list", ID_PLACEMENT_LINK: "massedit-placement-link", // Modal footer ids ID_MODAL_CONTAINER: "massedit-modal-container", ID_MODAL_SUBMIT: "massedit-modal-submit", ID_MODAL_TOGGLE: "massedit-modal-toggle", ID_MODAL_CANCEL: "massedit-modal-cancel", ID_MODAL_PREVIEW: "massedit-modal-preview", ID_MODAL_CLEAR: "massedit-modal-clear", ID_MODAL_CLOSE: "massedit-modal-close", // Modal body ids ID_CONTENT_REPLACE: "massedit-content-replace", ID_CONTENT_ADD: "massedit-content-add", ID_CONTENT_MESSAGE: "massedit-content-message", ID_CONTENT_LIST: "massedit-content-list", ID_CONTENT_PREVIEW: "massedit-content-preview", ID_CONTENT_FORM: "massedit-content-form", ID_CONTENT_FIELDSET: "massedit-content-fieldset", ID_CONTENT_CONTENT: "massedit-content-content", ID_CONTENT_TARGET: "massedit-content-target", ID_CONTENT_INDICES: "massedit-content-indices", ID_CONTENT_PAGES: "massedit-content-pages", ID_CONTENT_SUMMARY: "massedit-content-summary", ID_CONTENT_SCENE: "massedit-content-scene", ID_CONTENT_ACTION: "massedit-content-action", ID_CONTENT_TYPE: "massedit-content-type", ID_CONTENT_CASE: "massedit-content-case", ID_CONTENT_LOG: "massedit-content-log", ID_CONTENT_BYLINE: "massedit-content-byline", ID_CONTENT_BODY: "massedit-content-body", ID_CONTENT_MEMBERS: "massedit-content-members", // Preview modal elements ID_PREVIEW_CONTAINER: "massedit-preview-container", ID_PREVIEW_TITLE: "massedit-preview-title", ID_PREVIEW_BODY: "massedit-preview-body", ID_PREVIEW_CLOSE: "massedit-preview-close", ID_PREVIEW_BUTTON: "massedit-preview-button", // Toolbar placement classes CLASS_PLACEMENT_OVERFLOW: "overflow", // Preview classes CLASS_PREVIEW_BUTTON: "massedit-preview-button", // Modal footer classes CLASS_MODAL_CONTAINER: "massedit-modal-container", CLASS_MODAL_BUTTON: "massedit-modal-button", CLASS_MODAL_LEFT: "massedit-modal-left", CLASS_MODAL_OPTION: "massedit-modal-option", CLASS_MODAL_TIMER: "massedit-modal-timer", // Modal body classes CLASS_CONTENT_CONTAINER: "massedit-content-container", CLASS_CONTENT_FORM: "massedit-content-form", CLASS_CONTENT_FIELDSET: "massedit-content-fieldset", CLASS_CONTENT_TEXTAREA: "massedit-content-textarea", CLASS_CONTENT_INPUT: "massedit-content-input", CLASS_CONTENT_DIV: "massedit-content-div", CLASS_CONTENT_SPAN: "massedit-content-span", CLASS_CONTENT_SELECT: "massedit-content-select", }),   },    /**     * @description This pseudo-enum of the   namespace object     * is used to store a pair of arrays denoting which user groups are     * permitted to make use of the editing and messaging functionality (all * users are permitted to generate lists). For the purposes of forstalling    * the use of the script for vandalism or spam, its use is limited to     * certain members of local staff, various global groups, and Fandom Staff.     * The only major local group prevented from using the editing function is     * the   group, as these can be viewed as     * standard users with /d and thread-specific abilities. However, these     * users are permitted to make use of the mass-messaging functionality and     * can generate lists like other users.     *     * @readonly     * @enum {Array }     */    UserGroups: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ CAN_EDIT: Object.freeze([         "sysop",          "autoconfirmed",          "content-moderator",          "bot",          "bot-global",          "staff",          "vstf",          "helper",          "vanguard",          "wiki-manager",          "content-team-member",          "content-volunteer",        ]), CAN_MESSAGE: Object.freeze([         "global-discussions-moderator",          "threadmoderator",        ]), }),   },    /**     * @description The   pseudo-enum of the     *   namespace object is used to store several constants of     * the   data type related to standard edit interval rates     * and edit delays in cases of rate limiting. Originally, these were housed     * in a   object in the script-global namespace, though     * their exclusive use by the MassEdit class instance made their inclusion     * into   seem like a more sensible placement decision.     *     * @readonly     * @enum {number}     */    Utility: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ MAX_SUMMARY_CHARS: 64000, FADE_INTERVAL: 1000, DELAY: 10000, }),   },    /**     * @description The   pseudo-enum is used to store the     *   names of the four major operations supported by the     * MassEdit script, namely find-and-replace, append/prepend content, message     * users, and generation of page listings. These are used in the definition     * of certain element selectors and scene-specific elements, and their order     * is used to determine order in the main dropdown elements used to switch     * between scenes.     *     * @readonly     * @enum {Array }     */    Scenes: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze([ "replace", // Find-and-replace (1st scene, default) "add",     // Append/prepend content (2nd scene) "message", // Mass-message users (3rd scene) "list",    // List cat/ns members (4th scene) ]),   }  });  /****************************************************************************/  /*                           Setup pseudo-enums                             */ /****************************************************************************/ // Protected pseudo-enums of script setup object Object.defineProperties(init, {   /**     * @description This pseudo-enum of the   namespace object     * used to initialize the script stores data related to the external     * dependencies and core modules required by the script. It consists of two     * properties. The former, a constant   called "SCRIPTS,"     * contains key/value pairs wherein the key is the specific name of the     *   and the value is the script's location for use by     *  . The latter, a constant array named     * , contains a listing of the core modules required for     * use by  .     *     * @readonly     * @enum {object}     */    Dependencies: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ SCRIPTS: Object.freeze({         // Keys should NOT be altered unless hook names change          "dev.i18n": "u:dev:MediaWiki:I18n-js/code.js",          "dev.placement": "u:dev:MediaWiki:Placement.js",          "dev.modal": "u:dev:MediaWiki:Modal.js",          "dev.enablewallext": "u:dev:MediaWiki:WgMessageWallsExist.js",        }), MODULES: Object.freeze([         "mediawiki.util",          "mediawiki.user",          "ext.wikia.LinkSuggest",        ]), }),   },    /**     * @description This pseudo-enum of the   namespace object     * is used to store default data pertaining to the Placement.js external     * dependency. It includes an   denoting the default     * placement location for the script in the event of the user not including     * any user config and an array containing the two valid placement types. By     * default, the script tool element as built in   is     * appended to the user toolbar.     *     * @readonly     * @enum {object}     */    Placement: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ DEFAULTS: Object.freeze({         ELEMENT: "tools",          TYPE: "prepend",        }), VALID_TYPES: Object.freeze([         "append",          "prepend",        ]), }),   },    /**     * @description This catchall pseudo-enum of the   constant denoting the     * name of the script.     *     * @see SUS-4775     * @see VariablesBase.php     * @readonly     * @enum {string|number}     */    Utility: {      enumerable: true,      writable: false,      configurable: false,      value: Object.freeze({ SCRIPT: "MassEdit", STD_INTERVAL: 1500, BOT_INTERVAL: 750, CACHE_VERSION: 2, }),   }  });  /****************************************************************************/  /*                      Prototype Utility methods                           */ /****************************************************************************/ /**   * @description As the name implies, this helper function capitalizes the * first character of the input string and returns the altered, adjusted * string. it is generally used in the dynamic construction of i18n messages * in various assembly methods. *  * @param {string} paramTarget -   to be capitalized * @returns {string} - Capitalized */ main.capitalize = function (paramTarget) { return paramTarget.charAt(0).toUpperCase + paramTarget.slice(1); }; /**   * @description This helper method is used to check whether the target object * is one of several types of. It is most often used to  * determine if the target is an   or a straight-up * .   *   * @param {string} paramType - Either "Object" or "Array" * @param {string} paramTarget - Target to check * @returns {boolean} - Flag denoting the nature of the target */ main.isThisAn = function (paramType, paramTarget) { return Object.prototype.toString.call(paramTarget) === "[object " + this.capitalize.call(this, paramType.toLowerCase) + "]"; }; /**   * @description This function is used to determine whether or not the input *  contains restricted characters as denoted by Wikia's   *. Legal characters are defined as follows: *    *   * @param {string} paramString Content string to be checked * @return {boolean} - Flag denoting the nature of the paramString */ main.isLegalInput = function (paramString) { return new RegExp("^[" + wk.wgLegalTitleChars + "]*$").test(paramString); }; /**   * @description This helper function uses simple regex to determine whether * the parameter  or   is an integer * value. It is primarily used to determine if the user has inputted a proper * namespace number if mass editing by namespace. *  * @param {string|number} paramEntry - Namespace number * @returns {boolean} - Flag denoting the nature of the paramEntry */ main.isInteger = function (paramEntry) { return new RegExp(/^[0-9]+$/).test(paramEntry.toString); }; /**   * @description This function serves as an Internet Explorer-friendly * implementation of, a method * introduced in ES2015 and unavailable to IE 11 and earlier. It is based off * the polyfill available on the method's Mozilla.org documentation page. *  * @param {string} paramTarget -   to be checked * @param {string} paramSearch -  target * @returns {boolean} - Flag denoting a match */ main.startsWith = function (paramTarget, paramSearch) { return paramTarget.substring(0, 0 + paramSearch.length) === paramSearch; }; /**   * @description This utility method is used to check whether the user * attempting to use the script is in the proper usergroup. Only certain local * staff and members of select global groups are permitted the use of the * editing and messaging functionality so as to prevent potential vandalism, * though any users are permitted to generate lists of category members. *  * @param {boolean} paramMessaging - Whether to include messaging groups * @return {boolean} - Flag denoting user's ability to use the script */ main.hasRights = function (paramMessaging) { return new RegExp(["(" + this.UserGroups.CAN_EDIT.join("|") + ((paramMessaging) ? "|" + this.UserGroups.CAN_MESSAGE.join("|") : "") + ")"].join("")).test(wk.wgUserGroups.join(" ")); }; /**   * @description This utility method is used to remove duplicate entries from * an array prior to the usage of the Quicksort implementation included in  * the sections below. Unlike other duplicate-removal implementations, this * version makes no use of  or any * value comparisons to determine if elements are already in a temporary * storage structure. Instead, each element of the parameter array is simply * added to the temporary object as a key, overwriting any previously added * keys of the same value. This results in an object with unique keys that can * be collated into an array and returned from the function. *  * @param {Array } paramArray - Array with potential duplicates * @returns {Array } - Duplicate-free array ready for sorting */ main.replaceDuplicates = function (paramArray) { // Declarations var i, n, tempObject; // Add unique elements as keys of local object for (i = 0, n = paramArray.length, tempObject = {}; i < n; i++) { tempObject[paramArray[i]] = true; }   // Grab unique keys from tempObject as array return Object.keys(tempObject); }; /**   * @description This helper function is used as the primary mechanism by which * the find-and-replace operation is undertaken. It can either replace all * instances of a substring from an input parameter or only certain instances * as denoted by an optional parameter array of  indices. * Assuming such a parameter array is passed, a callback function is used with *  in order to sort through the * appearances of the target in the content and adjust only those at one of  * the desired indices. In either case, the ammended string is adjusted and * returned for posting by means of the API as the pages's new adjusted * content. *  * @param {string} paramString - Original string to be adjusted * @param {boolean} paramIsCaseSensitive - If case sensitivity is desired * @param {string} paramTarget - Text to be replaced * @param {string} paramReplacement - Text to be inserted * @param {Array } paramInstances - Indices at which to replace text * @returns {string} - An ammended */ main.replaceOccurrences = function (paramString, paramIsCaseSensitive,      paramTarget, paramReplacement, paramInstances) { // Declarations var counter, regex; // Definitions/sanitize params paramInstances = (paramInstances != null) ? paramInstances : []; regex = new RegExp(     paramTarget        .replace(/\r/gi, "")        .replace(/([.*+?^=!:${}|\[\]\/\\])/g, "\\$1"),      ((paramIsCaseSensitive) ? "g" : "gi") + "m"   ); counter = 0; // Replace all instances if no specific indices specified return paramString.replace(regex, (!paramInstances.length)     ? paramReplacement      : function (paramMatch) {          return ($.inArray(++counter, paramInstances) !== -1)            ? paramReplacement            : paramMatch;        }    ); }; /****************************************************************************/  /*                       Prototype Dynamic Timer                            */ /****************************************************************************/ /**   * @description This function serves as a pseudo-constructor for the pausable *  iterator. It accepts a function as a  * callback and an edit interval, setting these as publically accessible * function properties alongside other default flow control * s. The latter are used elsewhere in the program to   * determine whether or not event listener handlers can be run, as certain * handlers should not be accessible if an editing operation is in progress. *  * @param {function} paramCallback - Function to run after interval complete * @param {number} paramInterval - Rate at which timeout is handled * @returns {object} self - Inner object return for external assignment */ main.setDynamicTimeout = function self (paramCallback, paramInterval) { // Define pseudo-instance properties from args self.callback = paramCallback; self.interval = paramInterval; // Set flow control booleans self.isPaused = false; self.isComplete = false; // Set default value for id   self.identify = -1; // Begin first iterate and define id   self.iterate; // Return for definition to local variable return self; }; /**   * @description This internal method of the * function is used to cancel any ongoing editing operation by clearing the * current timeout and setting the  flow control *  to true. This lets external handlers know that the * editing operation is complete, enabling or disabling them in turn. *  * @returns {void} */ main.setDynamicTimeout.cancel = function  { this.isComplete = true; wk.clearTimeout(this.identify); }; /**   * @description This internal method of the * function is used to pause any ongoing editing operation by setting the *  flow control   and clearing the * current  identified. This is  * called whenever the user presses the   modal button. *  * @returns {void} */ main.setDynamicTimeout.pause = function  { if (this.isPaused || this.isComplete) { return; }   this.isPaused = true; wk.clearTimeout(this.identify); }; /**   * @description This internal method of the * function is used to resume any ongoing and paused editing operation by  * setting the   flow control   to   *   and calling the   method to proceed * to the next iteration. It is called when the user presses "Resume." *  * @returns {void} */ main.setDynamicTimeout.resume = function  { if (!this.isPaused || this.isComplete) { return; }   this.isPaused = false; this.iterate; }; /**   * @description This internal method of the * function is used to proceed on to the next iteration by resetting the *  function property to the value returned by a new *  invocation. The function accepts as an optional * parameter an interval rate greater than that defined as in the function * instance property  for cases of ratelimiting. In such * a case, the rate is extended to 35 seconds before the callback is called. *  * @param {number} paramInterval - Optional interval rate parameter * @returns {void} */ main.setDynamicTimeout.iterate = function (paramInterval) { if (this.isPaused || this.isComplete) { return; }   // Interval should only be greater than instance interval paramInterval = (paramInterval < this.interval || paramInterval == null) ? this.interval : paramInterval; // Define the identifier this.identify = wk.setTimeout(this.callback, paramInterval); }; /****************************************************************************/  /*                           Prototype Quicksort                            */ /****************************************************************************/ /**   * @description This implementation of the classic Quicksort algorithm is used * to quickly sort through a listing of category or namespace member pages * prior to iteration or display. Originally, the author went with the default *  native code. However, after running speed * tests between Chrome's V8 native implementation and a few custom Quicksort * algorithms, the author decided to go with a custom implementation. Current * speed tests for the native code generally result in sorting times of  * 700-900 ms for an array of 1,000,000  s, while this * custom implementation averages between 150-350 ms for the same data set. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left index * @param {number} paramRight - Parameter right index * @returns {Array } paramArray - Sorted array */ main.sort = function self (paramArray, paramLeft, paramRight) { // Declarations var args, index; // Convert to proper array args = Array.prototype.slice.call(arguments); // Failsafe to ensure all parameters have initial values if (args.length === 1 && this.isThisAn("Array", args[0])) { paramArray = args[0]; paramLeft = 0; paramRight = paramArray.length - 1; }   // Recursively partition and call self until sorted if (paramArray.length) { index = self.partition(paramArray, paramLeft, paramRight); if (paramLeft < index - 1) { self(paramArray, paramLeft, index - 1); }     if (index < paramRight) { self(paramArray, index, paramRight); }   }    return paramArray; }; /**   * @description One of two main helper functions of the Quicksort algorithm, *  is used, as the name implies, to swap the * elements included in the parameter array at the indices specified by  *   and. A temporary local * variable,, is used to faciliate the switch and store * the left pointer's value while the element at the right index is assigned * as the new left pointer's value. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left array index * @param {number} paramRight - Parameter right array index * @returns {void} */ main.sort.swap = function (paramArray, paramLeft, paramRight) { // Declaration var swapped; // Temporarily store left pointer's value swapped = paramArray[paramLeft]; // Set right pointer's value as new value at left index paramArray[paramLeft] = paramArray[paramRight]; // Former left value now set to the right paramArray[paramRight] = swapped; }; /**   * @description The second of two such helper functions of the Quicksort * algorithm, ,as its name implies, is used to   * divide the parameter array based on the values of the * index pointers. It is called often during 's set of   * divide-and-conquer recursive calls to further adjust the pointer values and * swap values accordingly while the left pointer value is less than the right * pointer index. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left index * @param {number} paramRight - Parameter right index * @returns {number} leftPointer - Leftmost pointer index */ main.sort.partition = function (paramArray, paramLeft, paramRight) { // Declarations var pivot, leftPointer, rightPointer; // Middlemost pivot element pivot = paramArray[Math.floor((paramLeft + paramRight) / 2)]; // Initial pointer definitions leftPointer = paramLeft; rightPointer = paramRight; while (leftPointer <= rightPointer) { // Adjust left pointer index while (paramArray[leftPointer] < pivot) { leftPointer++; }     // Adjust right pointer index while (paramArray[rightPointer] > pivot) { rightPointer--; }     // Switch the elements at the present point indices if (leftPointer <= rightPointer) { this.swap(paramArray, leftPointer++, rightPointer--); }   }    // For use as "index" local var in main.sort return leftPointer; }; /****************************************************************************/  /*                        Prototype API methods                             */ /****************************************************************************/ /**   * @description As the name of the method implies, this Nirvana function is   * used to return data related to the user indicated by the parameter * . It's perhaps important to note that the method will * return data about the user making the request if improper title characters * are used in the query, making it important to check s   * with   or     * prior to invocation. *  * @param {string} paramUsername - A   username * @returns {object} -  resolved promise */ main.getUsernameData = function (paramUsername) { return nv.getJson("UserProfilePage", "renderUserIdentityBox", {     title: paramUsername,    }); }; /**   * @description One of two such methods, this function is used to post an   * individual thread to the selected user's message wall. Returning a resolved *  promise, the function provides the data for testing and * logging purposes on a successful edit and returns the associated error if  * the operation was unsuccessful. This function is called from within the * main submission handler 's assorted *  handlers if message walls are enabled * on-wiki. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.postMessageWallThread = function (paramConfig) { return nv.postJson("WallExternal", "postNewMessage", $.extend(false, { token: mw.user.tokens.get("editToken"), pagenamespace: 1200, }, paramConfig)); }; /**   * @description The second such method, this function is responsible for * posting a new talk topic to the talk page of the selected user. Like the * function above, it returns a resolved  promise and * provides the data for testing and logging purposes on success and the * associated error on failed operations. It too is called from within the * main submission handler 's assorted *  handlers if message walls are not enabled * on the wiki in question. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.postTalkPageTopic = function (paramConfig) { return $.ajax({     type: "POST",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "edit", section: "new", format: "json", }, paramConfig),   }); }; /**   * @description This function is one of two that handle the previewing of a   * formatted message, with this specific function used if message walls are * enabled on the wiki. Like all of the data request functions in the script, * it returns a resolved  promise that provides the * invoking handler  with the parsed * contents of the message in question on successful preview operations or the * associated error on failed operations. *  * @param {string} paramBody - Content of the message * @returns {object} -  object */ main.previewMessageWallThread = function (paramBody) { return nv.postJson("WallExternal", "preview", {     body: paramBody,    }); }; /**   * @description Like its matched function above, this function is used to   * handle previewing of messages on wikis that do not have message walls * enabled. Like the rest of the querying functions, this particular instance * returns a resolved  promise that provides the invoking * handler function, namely, with the * parsed contents of the message in question returned on successful * previewing operations or the relevant error on failed operations. *  * @param {string} paramBody Content of the message * @returns {object} -  object */ main.previewTalkPageTopic = function (paramBody) { return $.ajax({     type: "POST",      url: mw.util.wikiScript("index"),      data: {        action: "ajax",        rs: "EditPageLayoutAjax",        page: "SpecialCustomEditPage",        method: "preview",        content: paramBody,      }    }); }; /**   * @description This function queries the API for member pages of a specific * namespace, the id of which is included as a property of the parameter * . This argument is merged with the default *  parameter object and can sometimes include properties * related to  requests for additional members * beyond the default 5000 max. The method returns a resolved *  promise for use in attaching related callbacks to   * handle the member pages. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.getNamespaceMembers = function (paramConfig) { return $.ajax({     type: "GET",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "query", list: "allpages", apnamespace: "*", aplimit: "max", format: "json", }, paramConfig)   }); }; /**   * @description This function queries the API for member pages of a specific * category, the id of which is included as a property of the parameter * . This argument is merged with the default *  parameter object and can sometimes include properties * related to  requests for additional members * beyond the default 5000 max. The method returns a resolved *  promise for use in attaching related callbacks to   * handle the member pages. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.getCategoryMembers = function (paramConfig) { return $.ajax({     type: "GET",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "query", list: "categorymembers", cmnamespace: "*", cmprop: "title", cmdir: "desc", cmlimit: "max", format: "json", }, paramConfig)   }); }; /**   * @description This function queries the API for data related to pages that * transclude/embed a given template somewhere. This argument is merged with * the default  parameter object and can sometimes include * properties related to  requests for additional * members beyond the default 5000 max. The method returns a resolved *  promise for use in attaching related callbacks to   * handle the member pages. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.getTemplateTransclusions = function (paramConfig) { return $.ajax({     type: "GET",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "query", list: "embeddedin", einamespace: "*", eilimit: "max", format: "json", }, paramConfig)   }); }; /**   * @description This function is used in cases of content find-and-replace to   * acquire the parameter page's text content. As with all * invocations, it returns a resolved  promise for use * in attaching handlers tasked with combing through the page's content once * returned. *  * @param {string} paramPage -   title of the page * @returns {object} -  resolved promise */ main.getPageContent = function (paramPage) { return $.ajax({     type: "GET",      url: mw.util.wikiScript("api"),      data: {        action: "query",        prop: "info|revisions",        intoken: "edit",        titles: paramPage,        rvprop: "content|timestamp",        rvlimit: "1",        indexpageids: "true",        format: "json"      }    }); }; /**   * @description This function is the primary means by which all edits are * committed to the database for display on the page. As with several of the * other API methods, this function is passed a config  for * merging with the default API parameter object, with parameter properties * differing depending on the operation being undertaken. Though it makes no  * difference for the average editor, the   property is set to   *. The function returns a resolved * promise for use in attaching handlers post-edit. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.postPageContent = function (paramConfig) { return $.ajax({     type: "POST",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "edit", bot: true, format: "json", }, paramConfig)   }); }; main.getValidatedEntries = function (paramEntries, paramType) { // Declarations var i, n, entry, results, $deferred, prefix; // Returnable array of valid pages results = []; // Returned $.Deferred $deferred = new $.Deferred; // Cats and templates get prefixes prefix = wk.wgFormattedNamespaces[{ categories: 14, namespaces: 0, templates: 10, }[paramType]] || ""; for (i = 0, n = paramEntries.length; i < n; i++) { // Cache value to prevent multiple map lookups entry = this.capitalize(paramEntries[i].trim); // If requires prefix but entry does not have prefix if (!this.startsWith(entry, prefix)) { entry = prefix + ":" + entry; }     // If legal page/category name, push into names array if (       (paramType !== "namespaces" && this.isLegalInput(entry)) ||        (paramType === "namespaces" && this.isInteger(entry))      ) { results.push(entry); } else { return $deferred.reject("logErrorSecurity"); }   }    if (!results.length) { // Error: No wellformed pages exist to edit $deferred.reject("logErrorNoWellformedPages"); } else { $deferred.resolve(results); }   return $deferred.promise; };  main.getExtantUsernames = function (paramEntries) { // Declarations var counter, names, entries, $getUser, $getUsers, $addUser, $returnUsers, wallPrefix, userPrefix; // Definitions $addUser = new $.Deferred; $returnUsers = new $.Deferred; // Iterator counter counter = 0; // Message Wall or User talk wallPrefix = wk.wgFormattedNamespaces[ (this.utility.hasMessageWalls) ? 1200       : 3    ] + ":";    // "User:" userPrefix = wk.wgFormattedNamespaces[3] + ":"; // Array of extant usernames with prefix names = []; entries = []; // Get wellformed, formatted namespace numbers or category names $getUsers = this.getValidatedEntries(paramEntries); // Once acquired, apply to names array or pass along rejection message $getUsers.then(function (paramResults) {     names = paramResults;    }, $returnUsers.reject.bind($)); // Log paramResults if (DEBUG) { console.log(names); }   // Indicate checking is in progress $returnUsers.notify("logStatusCheckingUsernames"); // Iterate over provided list of usernames this.utility.timer = this.setDynamicTimeout(function {      if (counter === names.length) {        if (entries.length) {          // Return Quicksorted entries          return $returnUsers.resolve(this.sort(entries)).promise;        } else {          // Error: No wellformed pages exist to edit          return $returnUsers.reject("logErrorNoWellformedUsernames").promise;        }      }      // Acquire member pages of cat or ns      $getUser = $.when(this.getUsernameData(userPrefix + names[counter]));      // Once acquired, add pages to array      $getUser.always($addUser.notify);    }.bind(this), this.interval);

$addUser.progress(function (paramResults, paramStatus, paramXHR) {     if (DEBUG) {        console.log(paramResults, paramStatus, paramXHR);      }      if (paramStatus !== "success" || paramXHR.status !== 200) {        $returnUsers.notify("logErrorNoUserData", names[counter++]);        return this.utility.timer.iterate;      }      if (paramResults.user && paramResults.user.edits !== -1) {        $returnUsers.notify("logSuccessUserExists", names[counter]);        entries.push(wallPrefix + names[counter++]);      } else {        $returnUsers.notify("logErrorNoSuchPage", names[counter++]);      }      return this.utility.timer.iterate;    }.bind(this)); return $returnUsers.promise; }; main.getMemberPages = function (paramEntries, paramType) { // Declarations var i, n, names, data, entries, parameters, counter, config, $getPages, $addPages, $getEntries, $returnPages; // New pending Deferred objects $returnPages = new $.Deferred; $addPages = new $.Deferred; // Iterator index for setTimeout counter = 0; // getCategoryMembers or getNamespaceMembers param object parameters = {}; // Arrays names = [];    // Store names of user entries entries = [];  // New entries to be returned config = { categories: { query: "categorymembers", handler: "getCategoryMembers", continuer: "cmcontinue", target: "cmtitle", },     namespaces: { query: "allpages", handler: "getNamespaceMembers", continuer: "apfrom", target: "apnamespace", },     templates: { query: "embeddedin", handler: "getTemplateTransclusions", continuer: "eicontinue", target: "eititle", },   }[paramType]; // Get wellformed, formatted namespace numbers, category names, or templates $getEntries = this.getValidatedEntries(paramEntries, paramType); // Once acquired, apply to names array or pass along rejection message $getEntries.then(function (paramResults) {     names = paramResults;    }, $returnPages.reject.bind($)); // Iterate over user input entries this.utility.timer = this.setDynamicTimeout(function {      if (counter === names.length) {        $addPages.resolve;        if (entries.length) {          // Remove all duplicates prior to sorting          entries = this.sort(this.replaceDuplicates(entries));          // Return Quicksorted entries bereft of duplicates          return $returnPages.resolve(entries).promise;        } else {          // Error: No wellformed pages exist to edit          return $returnPages.reject("logErrorNoWellformedPages").promise;        }      }      // Set parameter target page      parameters[config.target] = names[counter];      // Fetching member pages of $1 or Fetching transclusions of $1      $returnPages.notify((paramType === "templates") ? "logStatusFetchingTransclusions" : "logStatusFetchingMembers", names[counter]);     // Acquire member pages of cat or ns or transclusions of templates      $getPages = $.when(this[config.handler](parameters));      // Once acquired, add pages to array      $getPages.always($addPages.notify);    }.bind(this), this.interval); $addPages.progress(function (paramResults, paramStatus, paramXHR) {     if (DEBUG) {        console.log(paramResults, paramStatus, paramXHR);      }      if (paramStatus !== "success" || paramXHR.status !== 200) {        $returnPages.notify("logErrorFailedFetch", names[counter++]);        return this.utility.timer.iterate;      }      // Define data      data = paramResults.query[config.query];      // If page doesn't exist, add log entry and continue to next iteration      if (data == null || data.length === 0) {        $returnPages.notify("logErrorNoSuchPage", names[counter++]);        return this.utility.timer.iterate;      }      // Add extant page titles to the appropriate submission property      for (i = 0, n = data.length; i < n; i++) {        entries.push(data[i].title);      }      // Only iterate counter if current query has no more extant pages      if ( paramResults["query-continue"] || paramResults.hasOwnProperty("query-continue") ) {       parameters[config.continuer] =          paramResults["query-continue"][config.query][config.continuer];      } else {        parameters = {};        counter++;      }      // On to the next iteration      return this.utility.timer.iterate;    }.bind(this)); return $returnPages.promise; }; main.assembleElement = function (paramArray) { // Declarations var type, attributes, counter, content; // Make sure input argument is a well-formatted array if (!this.isThisAn("Array", paramArray)) { return this.assembleElement.call(this,       Array.prototype.slice.call(arguments)); }   // Definitions counter = 0; content = ""; type = paramArray[counter++]; // mw.html.element requires an object for the second param attributes = (this.isThisAn("Object", paramArray[counter])) ? paramArray[counter++] : {};   while (counter < paramArray.length) { // Check if recursive assembly is required for another inner DOM element content += (this.isThisAn("Array", paramArray[counter])) ? this.assembleElement(paramArray[counter++]) : paramArray[counter++]; }   return mw.html.element(type, attributes, new mw.html.Raw(content)); };

main.assembleOverflowElement = function (paramText) { return this.assembleElement(     ["li", {        "class": this.Selectors.CLASS_PLACEMENT_OVERFLOW,        "id": this.Selectors.ID_PLACEMENT_LIST,       },        ["a", {          "id": this.Selectors.ID_PLACEMENT_LINK,          "href": "#",          "title": paramText,        },          paramText,        ],      ]    ); };

main.assembleTextfield = function (paramName, paramType) { // Declarations var elementId, elementClass, prefix, placeholder, title, attributes; // Sanitize parameters paramName = paramName.toLowerCase; paramType = paramType.toLowerCase; // Definitions elementId = "ID_CONTENT_" + paramName.toUpperCase; elementClass = "CLASS_CONTENT_" + paramType.toUpperCase; // Message definitions prefix = "modal" + this.capitalize(paramName); placeholder = prefix + "Placeholder"; title = prefix + "Title"; attributes = { id: this.Selectors[elementId], class: this.Selectors[elementClass], placeholder: this.i18n.msg(placeholder).plain, };   if (paramType === "input") { attributes.type = "textbox"; }   return this.assembleElement(      ["div", {class: this.Selectors.CLASS_CONTENT_DIV},        ["span", {class: this.Selectors.CLASS_CONTENT_SPAN},          this.i18n.msg(title).escape,        ],        [paramType, attributes],      ]    ); };

main.assembleDropdown = function (paramName, paramValues, paramIndex) { // Declarations var i, n, titleMessage, optionMessage, prefix, options, value, attributes, selectedIndex; // Sanitize input paramName = paramName.toLowerCase; // Set parameter value or first option as index selectedIndex = wk.parseInt(paramIndex, 10) || 0; // Listing of selectable dropdown options options = ""; // Prefix used in title and default dropdown option prefix = "modal" + this.capitalize(paramName); // Message for span title titleMessage = this.i18n.msg(prefix).escape; // Assemble array of HTML option strings for (i = 0, n = paramValues.length; i < n; i++) { // Sanitize parameter value = paramValues[i].toLowerCase; // Option-specific message optionMessage = prefix + this.capitalize(value); // Attributes for option element attributes = { value: value, };     // Choose which element to list as selected if (i === selectedIndex) { attributes.selected = "selected"; }     options += this.assembleElement(        ["option", attributes,          this.i18n.msg(optionMessage).escape,        ]      ); }   return this.assembleElement(      ["div", {class: this.Selectors.CLASS_CONTENT_DIV},        ["span", {class: this.Selectors.CLASS_CONTENT_SPAN},          titleMessage,        ],        ["select", {          size: "1",          name: paramName,          id: this.Selectors["ID_CONTENT_" + paramName.toUpperCase],          class: this.Selectors.CLASS_CONTENT_SELECT,        },          options,        ],      ]    ); }; /****************************************************************************/  /*                        Prototype Modal methods                           */ /****************************************************************************/ // Utility methods

main.injectModalStyles = function { mw.util.addCSS(     "." + this.Selectors.CLASS_CONTENT_CONTAINER + " {" +        "margin: auto !important;" +        "position: relative !important;" +        "width: 96% !important;" +      "}" +      "." + this.Selectors.CLASS_CONTENT_SELECT + "," +      "." + this.Selectors.CLASS_CONTENT_TEXTAREA + "," +      "." + this.Selectors.CLASS_CONTENT_INPUT + " {" +        "width: 99.6% !important;" +        "padding: 0 !important;" +        "resize: none !important;" +      "}" +      "." + this.Selectors.CLASS_CONTENT_TEXTAREA + " {" +        "height: 45px !important;" +      "}" +      "#" + this.Selectors.ID_CONTENT_MESSAGE + " " +      "." + this.Selectors.CLASS_CONTENT_TEXTAREA + "," +      "#" + this.Selectors.ID_CONTENT_LIST + " " +      "." + this.Selectors.CLASS_CONTENT_TEXTAREA + " {" +        "height: 85px !important;" +      "}" +      "#" + this.Selectors.ID_CONTENT_ADD + " " + "." + this.Selectors.CLASS_CONTENT_TEXTAREA + " {" + "height: 65px !important;" + "}" +     "#" + this.Selectors.ID_CONTENT_LOG + " {" + "height: 45px !important;" + "width: 99.6% !important;" + "border: 1px solid !important;" + "font-family: monospace !important;" + "background: #FFFFFF !important;" + "color: #AEAEAE !important;" + "overflow: auto !important;" + "padding: 0 !important;" + "}" +     "." + this.Selectors.CLASS_MODAL_BUTTON + "{" + "margin-left: 5px !important;" + "font-size: 8pt !important;" + "}" +     "." + this.Selectors.CLASS_MODAL_LEFT + "{" + "float: left !important;" + "margin-left: 0px !important;" + "margin-right: 5px !important;" + "}" +     "#" + this.Selectors.ID_PREVIEW_CONTAINER + "{" + "border: 1px solid currentColor !important;" + "padding: 10px !important;" + "overflow: auto !important;" + "min-height: 250px !important;" + "}" +     "#" + this.Selectors.ID_PREVIEW_BODY + " .pagetitle {" + "display: none !important;" + "}" +     "#" + this.Selectors.ID_PREVIEW_TITLE + " h2 {" + "display: inline-block !important;" + "}" +     "#" + this.Selectors.ID_PREVIEW_CLOSE + "{" + "display: inline-block !important;" + "float: right !important;" + "}" +     "." + this.Selectors.CLASS_PREVIEW_BUTTON + "{" + "border: none !important;" + "background: none !important;" + "color: currentColor !important;" + "cursor: pointer !important;" + "}" +     "." + this.Selectors.CLASS_PREVIEW_BUTTON + ":hover," + "." + this.Selectors.CLASS_PREVIEW_BUTTON + ":focus," + "." + this.Selectors.CLASS_PREVIEW_BUTTON + ":active {" + "outline: none !important;" + "background: none !important;" + "text-decoration: underline !important;" + "}"   );  };  /**   * @description This one-size-fits-all helper function is used to log entries   * in the status log on the completion of some operation or other. Originally,   * three separate loggers were used following a Java-esque method overloading   * approach. However, this was eventually abandoned in favor of a single   * method that takes an indeterminate number of arguments at any time.   *   * @returns {void}   */  main.addModalLogEntry = function  {    $("#" + this.Selectors.ID_CONTENT_LOG).prepend( this.i18n.msg.apply(this,       (arguments.length === 1 && arguments[0] instanceof Array)          ? arguments[0]          : Array.prototype.slice.call(arguments)      ).escape + " ");  };  /**   * @description This helper function is a composite of several previously   * extant shorter utility functions used to reset the form element,   * enable/disable various modal buttons, and log messages. It is called in a   * variety of contexts at the close of editing operations,   * failed API requests, and the like. Though it does not accept any formal   * parameters, it does permit an indeterminate number of arguments to be   * passed if the invoking function wishes to log a status message. In such   * cases, the collated arguments are bound to a shallow array and passed to   *   for logging.   *   * @returns {void}   */  main.resetModal = function  {    // Cancel the extant timer if applicable if (this.utility.timer && !this.utility.timer.isComplete) { this.utility.timer.cancel; }   // Add log message if i18n parameters passed if (arguments.length) { this.addModalLogEntry(Array.prototype.slice.call(arguments)); }   // Reset the form $("#" + this.Selectors.ID_CONTENT_FORM)[0].reset; // Re-enable modal buttons and fieldset this.toggleModalComponentsDisable(false); }; main.toggleModalComponentsDisable = function (paramValue) { // Declarations var i, n, groupSet, current, $scene, isMessaging; // Definitions $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2); // Elements to disable/enable groupSet = [ {       target: "#" + this.Selectors.ID_CONTENT_FIELDSET, value: paramValue, },     {        target: "." + this.Selectors.CLASS_MODAL_OPTION, value: paramValue, },     {        target: "." + this.Selectors.CLASS_MODAL_TIMER, value: !paramValue, },     {        target: "#" + this.Selectors.ID_MODAL_PREVIEW, value: !isMessaging || paramValue, },   ];    for (i = 0, n = groupSet.length; i < n; i++) { current = groupSet[i]; $(current.target).prop("disabled", current.value); } };  // Preview methods /**  * @description Like the similar modal method  , * this function is invoked once the preview has been displayed to the user to  * ensure that all interactive elements are properly attached to their * relevant listeners. It supports messages containing collapsible content and * adds a relevant handler for the close button which removes the temporary * preview container element and redisplays the message scene again once * clicked. *  * @returns {void} */ main.attachPreviewEvents = function  { // Declarations var container, $button, $messaging; // Definitions container = "#" + this.Selectors.ID_PREVIEW_CONTAINER; $button = $("#" + this.Selectors.ID_PREVIEW_BUTTON); $messaging = $("#" + this.Selectors.ID_CONTENT_MESSAGE); // Support collapsibles mw.hook("wikipage.content").fire(     // mw.util.$content[0].id vs mw.util.$content.selector      $(container + " #" + mw.util.$content[0].id)); // Fade out of preview on click $button.on("click", this.handleClear.bind(this, { before: function { $(container).remove; $messaging.show; }.bind(this), after: this.toggleModalComponentsDisable.bind(this, false) })); };  /**   * @description Like the similar modal builder  , * this function returns a  HTML framework for the message * preview functionality to which the contents of the message and title are * added. Rather than recreate this  each time the user * wants to preview a new message, the contents of this function are stored to  * the   object property for caching and easier * retrieval. *  * @returns {string} - The assembled   of preview HTML */ main.buildPreviewContent = function  { return this.assembleElement(     ["div", {id: this.Selectors.ID_PREVIEW_CONTAINER},        ["div", {id: this.Selectors.ID_PREVIEW_TITLE},          ["h2", {},            "$1",          ],          ["div", {id: this.Selectors.ID_PREVIEW_CLOSE},            ["button", {              class: this.Selectors.CLASS_PREVIEW_BUTTON,              id: this.Selectors.ID_PREVIEW_BUTTON            },              "(" + this.i18n.msg("buttonClose").escape + ")",            ]          ]        ],        ["hr"],        ["div", {id: this.Selectors.ID_PREVIEW_BODY},         "$2",        ],      ]    ); }; /**   * @description Like the similar modal function  , * this function is used to display the preview container in the messaging * modal scene on presses of the "Preview" modal button. Rather than reset the * contents of the modal itself by means of  * , an action which would delete the * data used to construct the preview and require that the user reenter the * content on exiting out of the preview scene, this function instead hides * the main messaging scene and appends a temporary  to the * modal that is removed once the user closes the preview by means of the * exit button. *  * @param {string} paramBody - The contents of the message preview * @returns {void} */ main.displayPreview = function (paramBody) { // Declarations var $scene, contents, $byline, $messaging, $modal,isMessaging; // Definitions $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val; $messaging = $("#" + this.Selectors.ID_CONTENT_MESSAGE); $modal = $("#" + this.Selectors.ID_MODAL_CONTAINER + " > section"); isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2); // Ensure messaging scene is shown if (!isMessaging || !this.modal.modal) { return; }   // Preview modal scene contents contents = ((this.modal.preview != null)     ? this.modal.scenes.preview      : this.modal.scenes.preview = this.buildPreviewContent    ).replace("$1", $byline).replace("$2", paramBody); // Log preview HTML if (DEBUG) { console.log("Preview contents: ", contents); }   // Hide the messaging rather than reset the modal contents $messaging.hide; // Add the preview container $modal.append(contents); }; // Modal methods /**  * @description As with the preview-specific function above, namely *, this function serves the purposes of   * ensuring that all interactive elements in the various modal scenes are * provided their appropriate listeners. This function handles the disabling * of various components based on the actions performed and invokes *  for various elements that may * have wikitext link content on each scene change. *  * @returns {void} */ main.attachModalEvents = function  { // Declarations var i, n, field, elements; // Define elements to linksuggest elements = [ "ID_CONTENT_TARGET",   // Target replacement content "ID_CONTENT_CONTENT",  // New content to be added "ID_CONTENT_SUMMARY",  // Edit summary "ID_CONTENT_BODY",     // Message body ];   // Apply linksuggest to each element on focus event for (i = 0, n = elements.length; i < n; i++) { field = "#" + this.Selectors[elements[i]]; $(document).on("focus", field, $.prototype.linksuggest.bind($(field))); }   // Disable certain components this.toggleModalComponentsDisable(false); }; /**   * @description As with the similar preview-specific function *, this method builds a     * HTML framework to which will be added scene-specific element selectors and * body content. As with, this content is   * only created once within the body of  , its * value cached in a local variable for use with all scenes requiring * assembly and used in conjunction with * to make scene-specific adjustments. *  * @returns {string} - Assembled HTML string framework */ main.buildModalContent = function  { return this.assembleElement(     ["section", {        id: "$1",        class: this.Selectors.CLASS_CONTENT_CONTAINER,      },        ["form", {          id: this.Selectors.ID_CONTENT_FORM,          class: this.Selectors.CLASS_CONTENT_FORM,        },          ["fieldset", {id: this.Selectors.ID_CONTENT_FIELDSET},            "$2",            "$3",          ],          ["hr"],        ],        ["div", {class: this.Selectors.CLASS_CONTENT_DIV},          ["span", {class: this.Selectors.CLASS_CONTENT_SPAN},            this.i18n.msg("modalLog").escape,          ],          ["div", {            id: this.Selectors.ID_CONTENT_LOG,            class: this.Selectors.CLASS_CONTENT_DIV,          }],        ],      ]    ); }; /**   * @description This function is used to assemble the four main so-called * "scenes" that make up the body content of the  instance. * Originally made up of four methods, each handling its respective scene, * this method handles the creation of all scene  HTML * during  assembly, ensuring that all content is ready for * display on the user's interaction with the module dropdown menu. The * assembled contents are then stored in the  class instance * variable  for use throughout the script without the need * to reassemble their contents each time the scene changes. *

*

* Unfortunately, the function runs at O(n ^ 3) time in the assembly of all * required scenes. The author has considered revamping this function and only * creating scenes when the user requests a scene change by means of the * aforementioned scene dropdown menu, caching each new scene once created in  * the relevant instance variable object. *  * @returns {object} this.modal.scenes - Reference to instance variable prop */ main.buildModalScenes = function  { // Declarations var i, j, k, m, n, o, framework, scene, defaultArgs, dropdownArgs, data, elements, object, arrays; // Define scenes object this.modal.scenes = {}; // Default arguments defaultArgs = ["scene", this.Scenes]; data = [ [ // Replace {         handler: "assembleDropdown", parameterArrays: [ ["type", ["pages", "categories", "namespaces"]], ["case", ["sensitive", "insensitive"]], ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["target", "textarea"], ["indices", "input"], ["content", "textarea"], ["pages", "textarea"], ["summary", "input"], ]       }      ],      [ // Addition {         handler: "assembleDropdown", parameterArrays: [ ["action", ["prepend", "append"]], ["type", ["pages", "categories", "namespaces"]] ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["content", "textarea"], ["pages", "textarea"], ["summary", "input"], ]       }      ],      [ // Messaging {         handler: "assembleTextfield", parameterArrays: [ ["pages", "textarea"], ["byline", "input"], ["body", "textarea"], ]       }      ],      [ // Listing {         handler: "assembleDropdown", parameterArrays: [ ["type", ["categories", "namespaces", "templates"]] ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["pages", "textarea"], ["members", "textarea"], ]       }      ]    ];    // Basic modal form framework framework = this.buildModalContent; for (i = 0, n = this.Scenes.length; i < n; i++) { // Internal definitions scene = this.Scenes[i]; // New scene object this.modal.scenes[scene] = {}; // Make copy of defaults and add the index dropdownArgs = $.merge($.merge([], defaultArgs), [i]); // New defaultArgs object logged if (DEBUG) { console.log(dropdownArgs); }     // Init string HTML elements = ""; // Make it O(n^3) - go big or go home for (j = 0, m = data[i].length; j < m; j++) { object = data[i][j]; arrays = object.parameterArrays; for (k = 0, o = arrays.length; k < o; k++) { elements += this[object.handler].apply(this, arrays[k]); }     }      // Make use of modal framework to insert scene-specific HTML this.modal.scenes[scene] = framework .replace("$1", this.Selectors["ID_CONTENT_" + scene.toUpperCase]) .replace("$2", this.assembleDropdown.apply(this, dropdownArgs)) .replace("$3", elements); }   // Log instance variable modal's scenes property if (DEBUG) { console.log("modal.scenes:", this.modal.scenes); }   // Return reference for use in buildModal return this.modal.scenes; }; /**   * @description This method is used to create a new * instance that serves as the primary interface of the script. It sets all * click events, defines all modal  buttons in the modal, * and assembles all the so-called "scenes" related to the various operations * supported by MassEdit. Originally, this function also injected the modal * CSS styling prior to creation of the modal, though for the purposes of  * ensuring single responsibility for all functions, the styling was moved * into a separate function, namely. *  * @returns {object} - A new   instance */ main.buildModal = function  { return new wk.dev.modal.Modal({     content: this.buildModalScenes[this.Scenes[0]], // 1st scene is default      id: this.Selectors.ID_MODAL_CONTAINER,      size: "medium",      title: this.i18n.msg("buttonScript").escape,      events: {        submit: this.handleSubmit.bind(this),        toggle: this.handleToggle.bind(this),        preview: this.handlePreviewing.bind(this),        clear: this.handleClear.bind(this),        cancel: this.handleCancel.bind(this),      },      buttons: [        {          text: this.i18n.msg("buttonSubmit").escape,          event: "submit",          primary: true,          id: this.Selectors.ID_MODAL_SUBMIT,          classes: [            this.Selectors.CLASS_MODAL_BUTTON,            this.Selectors.CLASS_MODAL_OPTION,          ],        },        {          text: this.i18n.msg("buttonPause").escape,          event: "toggle",          primary: true, disabled: true, id: this.Selectors.ID_MODAL_TOGGLE, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_TIMER, ],       },        {          text: this.i18n.msg("buttonCancel").escape, event: "cancel", primary: true, disabled: true, id: this.Selectors.ID_MODAL_CANCEL, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_TIMER, ],       },        {          text: this.i18n.msg("buttonPreview").escape, event: "preview", primary: true, disabled: true, id: this.Selectors.ID_MODAL_PREVIEW, classes: [ this.Selectors.CLASS_MODAL_BUTTON, ],       },        {          text: this.i18n.msg("buttonClose").escape, event: "close", id: this.Selectors.ID_MODAL_CLOSE, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_LEFT, this.Selectors.CLASS_MODAL_OPTION, ],       },        {          text: this.i18n.msg("buttonClear").escape, event: "clear", id: this.Selectors.ID_MODAL_CLEAR, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_LEFT, this.Selectors.CLASS_MODAL_OPTION, ],       },      ],    });  };  /**   * @description This method is the primary mechanism by which the modal is   * displayed to the user. If the modal has not been previously assembled, the   * function constructs a new   instance via an invocation of   * , creates the modal, and attaches all the requisite   * event listeners related to enabling   and find-and-   * replace-specific modal elements (linksuggest is enabled for the content *  and the edit summary  ).   *

*

* Once all listeners have been attached, the new modal is displayed to the * user. If the modal has been assembled prior to method invocation, the * instance is displayed to the user and the method exits. *  * @returns {void} */ main.displayModal = function  { if (this.modal.modal != null) { this.modal.modal.show; return; }   // Apply modal CSS styles prior to creation this.injectModalStyles; // Construct new Modal instance this.modal.modal = this.buildModal; // Create, then apply all relevant listeners this.modal.modal.create.then(function {      // Apply initial linksuggest      this.attachModalEvents;      // Change scene depending on user needs      $(document).on("change", "#" + this.Selectors.ID_CONTENT_SCENE, this.handleClear.bind(this, true));     // Once events are set, display the modal      this.modal.modal.show;    }.bind(this)); // Log modal instance variable if (DEBUG) { console.log("this.modal: ", this.modal); } };  /****************************************************************************/  /*                      Prototype Event handlers                            */ /****************************************************************************/ /**   * @description Arguably the most important method of the program, this * function coordinates the entire mass editing process from the initial press * of the "Submit" button to the conclusion of the editing operation. The * entire workings of the process were contained within a single method to  * assist in maintaining readability when it comes time to invariably repair * bugs and broken functionality. The other two major methods used by this * function are  and *, both of which are used to sanitize input * and return wellformed loose member pages if applicable. *

*

* The function collates all extant user input added via * and  fields before running through a set of conditional * checks to determine if the user can continue with the requested editing * operation. If the user may proceed, the function makes use of a number of  *   promises to coordinate the necessary acquisition of   * wellformed pages for editing. In cases of categories/namespaces, member * pages are retrieved and added to the editing queue for processing. *

*

* As of the latest update implementing additional editing functionality, this * method was temporarily separated into four separate methods related to each * of the four scenes. However, as the same progression of sanitizing input, * acquiring pages, iterating over pages, and logging accordingly was * evidenced in all operations, these handlers were once again merged into * this single method to prevent copious amounts of copy/pasta. The only * operation that exits early and does not iterate is the listing operation, * which merely acquires lists of category members and prepends them to an  * element. *  * @returns {void} */ main.handleSubmit = function  { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }   // Declarations var $action, $type, $case, $content, $target, $indices, indices, $pages, pages, $byline, $summary, counter, config, data, pageIndex, newText, $getPages, $postPages, $getNextPage, $getPageContent, $postPageContent, error, $scene, isCaseSensitive, isReplace, isAddition, isMessaging, isListing, $members, $selected, $body, pagesType; // Dropdowns $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $action = $("#" + this.Selectors.ID_CONTENT_ACTION)[0]; $type = $("#" + this.Selectors.ID_CONTENT_TYPE)[0]; $case = $("#" + this.Selectors.ID_CONTENT_CASE)[0]; // Textareas/inputs $target = $("#" + this.Selectors.ID_CONTENT_TARGET).val; $indices = $("#" + this.Selectors.ID_CONTENT_INDICES).val; $content = $("#" + this.Selectors.ID_CONTENT_CONTENT).val; $pages = $("#" + this.Selectors.ID_CONTENT_PAGES).val; $summary = $("#" + this.Selectors.ID_CONTENT_SUMMARY).val; // For acquiring text of selected option $selected = $("#" + this.Selectors.ID_CONTENT_TYPE + " option:selected"); // Messaging exclusives $body = $("#" + this.Selectors.ID_CONTENT_BODY).val; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val; // Listing exclusive $members = $("#" + this.Selectors.ID_CONTENT_MEMBERS); // Cache frequently used boolean flags isReplace = ($scene.value === "replace" && $scene.selectedIndex === 0); isAddition = ($scene.value === "add" && $scene.selectedIndex === 1); isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2); isListing = ($scene.value === "list" && $scene.selectedIndex === 3); // Substitute for $1 in logErrorNoPages pagesType = ((isMessaging)     ? this.i18n.msg("modalTypeUsernames").escape      : $selected.text).toLowerCase; // If no scene selected (should not happen) if (!isReplace && !isAddition && !isMessaging && !isListing) { return; // Is not in the proper rights group } else if (!isListing && !this.hasRights(isMessaging)) { this.resetModal; this.addModalLogEntry("logErrorUserRights"); return; // Is either append/prepend with no content input included } else if (isAddition && !$content) { this.addModalLogEntry("logErrorNoContent"); return; // Is find-and-replace with no target content included } else if (isReplace && !$target) { this.addModalLogEntry("logErrorNoTarget"); return; // No pages included } else if (!$pages) { this.addModalLogEntry("logErrorNoPages", pagesType); return; // If edit summary is greater than permitted max of 800 characters } else if ($summary && $summary.length > this.Utility.MAX_SUMMARY_CHARS) { this.addModalLogEntry("logErrorOverlongSummary"); return; // If message title is not legal } else if (isMessaging && !this.isLegalInput($byline)) { this.addModalLogEntry("logErrorSecurity"); return; // If no message body is included } else if (isMessaging && !$body) { this.addModalLogEntry("logErrorMissingBody"); return; }   // Status log message, scene-dependent this.addModalLogEntry(     (isReplace || isAddition)        ? "logStatusEditing"        : (isMessaging)          ? "logStatusMessaging"          : "logStatusGenerating"    ); this.toggleModalComponentsDisable(true); // Find-and-replace specific variable definitions if (isReplace) { // Only wellformed integers should be included as f-n-r indices indices = $indices.split(",").map(function (paramEntry) {       if (this.isInteger(paramEntry.trim)) {          return wk.parseInt(paramEntry, 10);        }      }.bind(this)).filter(function (paramEntry) {        return paramEntry != null; // Avoid cases of [undefined]      }); // Whether not search and replace is case sensitive isCaseSensitive = ($case.selectedIndex === 0 &&       $case.value === "sensitive"); }   // Array of pages/categories/namespaces pages = $pages.split(/[\n]+/).filter(function (paramEntry) {     return paramEntry !== "";    }); // Page counter for setInterval counter = 0; // Default page editing parameters config = {}; // New pending status Deferreds $postPages = new $.Deferred; $getNextPage = new $.Deferred; // Log flag for inspection if (DEBUG) { console.log("hasMessageWalls: ", this.utility.hasMessageWalls); }   /**     * @description The     is used * to acquire either wellformed loose page titles or the titles of member * pages belonging to input categories or namespaces. In cases of user * messaging, the function makes use of      * functionality to determine whether the wiki on which the script is being * used has enabled message walls. This knowledge is required as the prefix * applied to username input will differ ("Message Wall:" vs "User talk:") * accordingly. Similar functionality can be glimpsed in    *. */   $getPages = new $.Deferred(function ($paramOuter) {      new $.Deferred(function ($paramInner) { (!isMessaging || this.utility.hasMessageWalls != null) ? $paramInner.resolve.promise : wk.wgMessageWallsExist.then(             function  {                return $paramInner.resolve(true).promise;              }.bind(this),              function  {                return $paramInner.resolve(false).promise;              }.bind(this)            ); }.bind(this)).then( function (paramHasWalls) { if (paramHasWalls != null) { this.utility.hasMessageWalls = paramHasWalls; }         // Get list of wellformed pages/usernames or member pages return this[ (isListing || (!isMessaging && $type.value !== "pages")) ? "getMemberPages" : (isMessaging) ? "getExtantUsernames" : "getValidatedEntries" ](pages, ($type != null) ? $type.value : null); }.bind(this) ).then( $paramOuter.resolve.bind($), // $getPages.done $paramOuter.reject.bind($), // $getPages.fail $paramOuter.notify.bind($)  // $getPages.progress );   }.bind(this)); /**    * @description The resolved   returns an array of     * loose pages from a namespace or category, or returns an array of checked * loose pages if the individual pages option is selected or usenames are * inputted. Once resolved, assuming the user is not simply generating a    * pages list,   uses a       * to iterate over the pages, optionally acquiring page content for * find-and-replace. Once done, an invocation of  calls *  to assemble the parameters needed to     * edit the page in question. Once all pages have been edited, the pending * s are resolved and the timer exited. */   $getPages.done(function (paramResults) {      pages = paramResults;      // Log pages list (members or wellformed pages)      if (DEBUG) {        console.log("$getPages: ", pages);      }      // Listing activities end once members are acquired and shown to the user      if (isListing) {        // Add category members to textarea        $members.text(pages.join("\n"));        $getNextPage.resolve;        $postPages.resolve("logSuccessListingComplete");        return;      }      // Iterate over pages      this.utility.timer = this.setDynamicTimeout(function  { if (counter === pages.length) { $getNextPage.resolve; $postPages.resolve("logSuccessEditingComplete"); } else { $getPageContent = (!isReplace) ? new $.Deferred.resolve({}).promise : this.getPageContent(pages[counter]); // Grab data, extend parameters, then edit the page $getPageContent.always($postPages.notify); }     }.bind(this), this.interval);    }.bind(this)); /**    * @description In the cases of failed loose page acquisitions, either from * a failed API GET request or from a lack of wellformed input loose pages, * the relevant log entry returned from the getter function's    *   is logged, the timer canceled, and the modal form * re-enabled by means of. */   $getPages.fail(this.resetModal.bind(this)); /**    * @description Whenever the getter function (  or     *  ) needs to notify its invoking function * of a new ongoing category/namespace member acquisition operation, the * returned status message is acquired and added to the modal log. */   $getPages.progress(this.addModalLogEntry.bind(this)); /**    * @description Once the     is     * resolved, indicating the completion of the requested mass edits, a final * status message is logged, the form reenabled and reset for a new * round, and the  timer canceled by means of     *. */   $postPages.always(this.resetModal.bind(this)); /**    * @description The   handler is used to extend the *  object with properties relevant to the action * being performed (i.e. addition, replace, or messaging). Once complete, * the modified page content is committed and the edit made by means of    * a scene-specific handler, namely either  , *, or. * Once the edit is complete and a resolved promise returned, *  pings the pending *  to log the relevant messages and iterate on to     * the next page to be edited. */   $postPages.progress(function (paramResults) {      if (DEBUG) {        console.log("$postPages results: ", paramResults);      }      // Addition parameters      if (isAddition) {        config = {          handler: "postPageContent",          parameters: {            title: pages[counter],            token: mw.user.tokens.get("editToken"),            summary: $summary,          }        };        // "appendtext" or "prependtext"        config.parameters[$action.value.toLowerCase + "text"] = $content;      // Find-and-replace parameters      } else if (isReplace) {        pageIndex = Object.keys(paramResults.query.pages)[0];        data = paramResults.query.pages[pageIndex];        // Return if page doesn't exist to the server        if (pageIndex === "-1") {          this.addModalLogEntry("logErrorNoSuchPage", pages[counter++]);          return this.utility.timer.iterate; }       config = { handler: "postPageContent", parameters: { title: pages[counter], text: data.revisions[0]["*"], basetimestamp: data.revisions[0].timestamp, startimestamp: data.starttimestamp, token: data.edittoken, summary: $summary, }       };        // Replace instances of chosen text with inputted new text newText = this.replaceOccurrences(config.parameters.text,         isCaseSensitive, $target, $content, indices); // Return if old & new revisions are identical in content if (newText === config.parameters.text) { // Error: No instances of $1 found in $2. this.addModalLogEntry("logErrorNoMatch", $target, pages[counter++]); return this.utility.timer.iterate; } else { config.parameters.text = newText; }     // Messaging parameters } else if (isMessaging) { config = [ {           handler: "postTalkPageTopic", parameters: { sectiontitle: $byline, text: $body, title: pages[counter], }         },          {            handler: "postMessageWallThread", parameters: { messagetitle: $byline, body: $body, pagetitle: pages[counter], }         },        ][+this.utility.hasMessageWalls]; }     // Log all config handlers and parameters if (DEBUG) { console.log("Config: ", config); }     // Deferred attached to posting of data $postPageContent = this[config.handler](config.parameters); $postPageContent.always($getNextPage.notify); }.bind(this));   /**     * @description The pending state       *   is pinged by   once     * an POST request is made and a resolved status       * returned. The   callback takes the resultant success/     * failure data and logs the relevant messages before moving the operation     * on to the iteration of the   timer. If the     * user has somehow been ratelimited, the function introduces a 35 second     * cooldown period before undertaking the next edit and pushes the unedited     * page back onto the   stack.     */    $getNextPage.progress(function (paramData) { if (DEBUG) { console.log("$getNextPage results: ", paramData); }     error = (paramData.error && paramData.error.code) ? paramData.error.code : "unknownerror"; // Success differs depending on status of message walls on wiki if (       ( (!isMessaging || (isMessaging && !this.utility.hasMessageWalls)) && paramData.edit && paramData.edit.result === "Success" ) ||       (          isMessaging && this.utility.hasMessageWalls && paramData.status )     ) {        this.addModalLogEntry("logSuccessEditing", pages[counter++]); } else if (error === "ratelimited") { // Show ratelimit message with the delay in seconds this.addModalLogEntry("logErrorRatelimited",         (this.Utility.DELAY / 1000).toString); // Push the unedited page back on the stack pages.push(pages[counter++]); } else { // Error: $1 not edited. Please try again. this.addModalLogEntry("logErrorEditing", pages[counter++]); }     // On to the next iteration this.utility.timer.iterate(       (error === "ratelimited")          ? this.Utility.DELAY          : null      ); }.bind(this)); };  /**   * @description This function serves as the primary event listener for presses   * of the "Preview" button available to users who are seeking to mass-message   * other users. From the end user's perspective, the button press should be   * met with the fading out of the messaging modal scene and the display of a   * parsed version of the title and associated message. On presses of the close   * button, the preview should fade out and be replaced by the messaging modal   * with all of the user's messaging input still displayed in the textfields.   *

*

* This function accomplishes this by checking the user's input and querying * the database via  to determine whether the * wiki on which the script is being used has enabled message walls. Depending * on this query, the methods used to render and display the preview will * change though the end results will be the same. The function will fade in  * and out using   and invoke the requisite *  to show the preview   and *  to handle collapsibles and other events. *  * @returns {void} */ main.handlePreviewing = function  { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }   // Declarations var $scene, $byline, $body, isMessaging, $previewMessage; // Definitions $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val; $body = $("#" + this.Selectors.ID_CONTENT_BODY).val; isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2); // Just in case... if (!isMessaging) { return; }   // Check for title if (!$byline) { this.addModalLogEntry("logErrorMissingByline"); return; // Check for body content } else if (!$body) { this.addModalLogEntry("logErrorMissingBody"); return; }   /**     * @description   handles the acquisition of     * parsed HTML content related to the user's input message body and title. * Naturally, in order to ensure that the proper API methods are invoked, * the script must determine if the wiki on which MassEdit is being used * has enabled message walls. Once the proper method has been invoked, the * resultant  containing the message body HTML is passed * to. */   $previewMessage = new $.Deferred(function ($paramOuter) {      new $.Deferred(function ($paramInner) { (this.utility.hasMessageWalls != null) ? $paramInner.resolve(this.utility.hasMessageWalls).promise : wk.wgMessageWallsExist.then(             function  {                return $paramInner.resolve(true).promise;              }.bind(this),              function  {                return $paramInner.resolve(false).promise;              }.bind(this)            ); }.bind(this)).then( function (paramHasWalls) { if (this.utility.hasMessageWalls == null) { this.utility.hasMessageWalls = paramHasWalls; }         return this[(paramHasWalls) ? "previewMessageWallThread" : "previewTalkPageTopic" ]($body); }.bind(this) ).then( $paramOuter.resolve.bind($), // $previewMessage.done $paramOuter.reject.bind($)  // $previewMessage.fail );   }.bind(this)); /**    * @description Upon successful completion of the preview request operation, * the results are logged and the  scene transition * method invoked. In such cases,  is invoked * following the fade out to hide the messaging scene and append a preview *  and   is invoked * following the post appending fade-in. */   $previewMessage.done(function (paramResults) {      if (DEBUG) {        console.log("$previewMessage results: ", paramResults);      }      // Bypass handleClear's default functionality via the functions object      this.handleClear({ before: this.displayPreview.bind(this, paramResults[         (this.utility.hasMessageWalls) ? "body" : "html"]), after: this.attachPreviewEvents.bind(this), });   }.bind(this)); /**    * @description In cases wherein the message preview has failed for some * reason, the message scene doesn't change. The only alteration is the * addition of a relevant status log message denoting a failed preview * request. */   $previewMessage.fail(this.addModalLogEntry.bind(this, "logErrorNoPreview")); }; /**   * @description The   is the primary click handler for * the "Pause/Resume" button used to toggle the iteration timer. Depending on  * whether or not the timer is in use in iterating through collated pages * requiring editing, the text of the button will change accordingly. Once * invoked, the method will either restart the timer during an iteration or  * pause it indefinitely. If the timer is not running, the method will exit. *  * @returns {void} */ main.handleToggle = function  { if (     !this.utility.timer ||      (this.utility.timer && this.utility.timer.isComplete)    ) { if (DEBUG) { console.dir(this.utility.timer); }     return; }   // Declarations var $toggle, config; // Definitions $toggle = $("#" + this.Selectors.ID_MODAL_TOGGLE); config = [ {       message: "logTimerPaused", text: "buttonResume", method: "pause", },     {        message: "logTimerResume", text: "buttonPause", method: "resume", }   ][+this.utility.timer.isPaused]; // Add status log entry this.addModalLogEntry(config.message); // Change the text of the button $toggle.text(this.i18n.msg(config.text).escape); // Either resume or pause the setDynamicTimeout this.utility.timer[config.method]; }; /**   * @description Similar to , this function is used to   * cancel the timer used to iterate through pages requiring editing. As such, * it cancels the timer, adds a relevant status log entry, and re-enables the * standard editing buttons in the modal. If the timer is  * presently not running, the method simply returns and exits. The timer is  * logged in the console if   is set to. *  * @returns {void} */ main.handleCancel = function  { if (     !this.utility.timer ||      (this.utility.timer && this.utility.timer.isComplete)    ) { if (DEBUG) { console.dir(this.utility.timer); }     return; } else { this.resetModal("logTimerCancel"); } };  /**   * @description As the name implies, the   listener is   * mainly used to clear modal contents and reset the   HTML * element. Rather than simply invoke the helper function *, however, this function adds some animation by   * disabling the button set and fading in and out of the modal body during the * clearing operation, displaying a status message in the log upon completion. *

*

* In addition to its main responsibility of clearing the modal fields of  * content, the function is also used as the primary means of transitioning * between scenes on changes to the scene dropdown. It can even accept in  * place of a "transitioning"   input flag a dedicated * functions  containing handlers for the fade-in/fade-out * progression, bypassing all other internal functionality apart from the * core fade operation. *  * @param {boolean|object} paramInput - Flag or handler * @returns {void} */ main.handleClear = function (paramInput) { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }   // Declarations var $scene, functions, visible, hidden, isTransitioning; // Whether the function is being used to reset or scene transition isTransitioning = (typeof paramInput !== "boolean") ? false : paramInput; // Scene dropdown element $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; // $.prototype.animate objects visible = {opacity: 1}; hidden = {opacity: 0}; // Define listeners for fade-in and fade-out (either custom or defaults) functions = (     this.isThisAn("Object", paramInput) &&      paramInput.hasOwnProperty("before") &&      paramInput.hasOwnProperty("after")    ) ? paramInput : [         { // Standard form clear before: this.resetModal.bind(this), after: this.addModalLogEntry.bind(this, "logSuccessReset") },         { // Scene transition before: this.modal.modal.setContent.bind(this.modal.modal,             this.modal.scenes[$scene.value]), after: this.attachModalEvents.bind(this), }       ][+isTransitioning]; // Disable all modal buttons for duration of fade and reset $("." + this.Selectors.CLASS_MODAL_BUTTON).prop("disabled", true); // Fade out on modal and reset content before fade-in $("#" + this.Selectors.ID_MODAL_CONTAINER + " > section") .animate(hidden, this.Utility.FADE_INTERVAL, functions.before) .animate(visible, this.Utility.FADE_INTERVAL, functions.after); }; /****************************************************************************/  /*                     Prototype Pseudo-constructor                         */ /****************************************************************************/ /**   * @description The confusingly named   function serves * as a pseudo-constructor of the MassEdit class instance .Through the *  passed to  's invocation of   *   sets the  , *, and   instance properties, this * function sets default values for  and * and defines the toolbar element and its associated event listener, namely * .   *

*

* Following this function's invocation, the MassEdit class instance will have * a total of five instance variables, namely, , *,  ,  , and * . All other functionality related to MassEdit is stored * in the class instance prototype, the  namespace object, * for convenience. *  * @returns {void} */ main.init = function  { // Declarations var $toolItem, toolText; // I18n config for wiki's content language this.i18n.useContentLang; // Initialize new modal property this.modal = {}; // New helper object for local config this.utility = {}; // Initialize a new dynamic timer object this.utility.timer = null; // New instance boolean property this.utility.hasMessageWalls = null; // View instance props and prototype if (DEBUG) { console.dir(this); }   // Text to display in the tool element toolText = this.i18n.msg("buttonScript").plain; // Build tool item (nested link inside list element) $toolItem = $(this.assembleOverflowElement(toolText)); // Display the modal on click $toolItem.on("click", this.displayModal.bind(this)); // Either append or prepend the tool to the target $(this.placement.element)[this.placement.type]($toolItem); }; /****************************************************************************/  /*                         Setup Helper methods                             */ /****************************************************************************/ /**   * @description The first of two user input validators, this function is used * to ensure that the user's included config details related to Placement.js  * are wellformed and legitimate. MassEdit.js offers support for all of  * Placement.js's default element locations, though as a nod to the previous * incarnation of the script, the default placement element is the toolbar and * the default type is "append." In the event of an error being caught due to  * a malformed element location or a missing type, the default config options * housed in  are used instead to   * ensure that user input mistakes are handled somewhat gracefully. *  * @param {object} paramConfig - Placement.js-specific config * @returns {object} config - Adjusted Placement.js config */ init.definePlacement = function (paramConfig) { // Declarations var config, loader; // Definitions config = {}; loader = wk.dev.placement.loader; try { config.element = loader.element(paramConfig.element); } catch (e) { config.element = loader.element(this.Placement.DEFAULTS.ELEMENT); }   try { config.type = loader.type(       (this.Placement.VALID_TYPES.indexOf(paramConfig.type) !== -1)          ? paramConfig.type          : this.Placement.DEFAULTS.TYPE      ); } catch (e) { config.type = loader.type(this.Placement.DEFAULTS.TYPE); }   // Set script name loader.script(this.Utility.SCRIPT); return config; }; /**   * @description The second of the two validator functions used to check that * user input is wellformed and legitimate, this function checks the user's  * edit interval value against the permissible values for standard users and * flagged bot accounts. In order to ensure that the operations are carried * out smoothly, the user's rate is adjusted if it exceeds the edit * restrictions placed upon accounts of different user rights levels. The * original incarnation of this method came from a previous version of  * MassEdit which made use of a similar, jankier system to ensure the smooth * progression through all included pages without loss of required edits. *  * @see SUS-4775 * @see VariablesBase.php * @param {number} paramInterval - User's input interval value * @return {number} - Adjusted interval */ init.defineInterval = function (paramInterval) { if (     wk.wgUserGroups.indexOf("bot") !== -1 &&      (paramInterval < this.Utility.BOT_INTERVAL || wk.isNaN(paramInterval))    ) { return this.Utility.BOT_INTERVAL; // Reset to max 80 edits/minute } else if (     wk.wgUserGroups.indexOf("user") !== -1 &&      (paramInterval < this.Utility.STD_INTERVAL || wk.isNaN(paramInterval))    ) { return this.Utility.STD_INTERVAL; // Reset to max 40 edits/minute } else { return wk.parseInt(paramInterval, 10); } };  /****************************************************************************/  /*                          Setup Primary methods                           */ /****************************************************************************/ /**   * @description The confusingly named   function is used * to coordinate the script setup madness in a single method, validating all * user input by means of helper method invocation and setting all instance * properties of the MassEdit class instance. Once the *   has been assembled containing the relevant instance * variables for placement, edit interval, and i18n messages, the method calls *  to construct a new MassEdit class instance, * passing the  and the   namespace *  as the instance's prototype. *

*

* The separation of setup code and MassEdit functionality code into distinct * namespace s helped to ensure that code was logically * organized per the single responsibility principle and more readable by  * virtue of the fact that each namespace handles distinctly different tasks. * This will assist in debugging should an issue arise with either the setup * or the script's functionality itself. *  * @param {object} paramLang - i18n   returned from hook * @returns {void} */ init.main = function (paramLang) { // Declarations var i, n, array, descriptor, parameter, lowercase, method, property, descriptorProperties, configObject; // Two of the three local instance variables array = ["Interval", "Placement"]; // Support both MassEdit config and legacy Message config configObject = wk.MassEditConfig || wk.configMessage || {}; if (DEBUG) { console.log("Config: ", configObject); }   // New Object.create descriptor object descriptor = {}; descriptorProperties = { enumerable: true, configurable: false, writable: false, };   // Set I18n object as instance property descriptor.i18n = $.extend(true, {}, descriptorProperties); descriptor.i18n.value = paramLang; // Reduce copy pasta for (i = 0, n = array.length; i < n; i++) { // Definitions property = array[i]; method = "define" + property; lowercase = property.toLowerCase; parameter = (configObject.hasOwnProperty(lowercase)) ? configObject[lowercase] : null; // New descriptor entry descriptor[lowercase] = $.extend(true, {}, descriptorProperties); // Define descriptor entry value descriptor[lowercase].value = this[method](parameter); }   // Log init object for inspection if (DEBUG) { console.dir(init); }   // Create new MassEdit instance Object.create(main, descriptor).init; }; /**   * @description This function is invoked as many times as there are external * dependencies, serving as the primary hook handler for each of the required * events denoted in. Once all * dependencies have been successfully loaded and the hooks fired, the * function loads I18n-js messages and invokes  as   * the callback function. *  * @returns {void} */ init.load = function  { if (++this.loaded === Object.keys(this.Dependencies.SCRIPTS).length) { wk.dev.i18n.loadMessages(this.Utility.SCRIPT, {       cacheVersion: this.Utility.CACHE_VERSION,      }).then(this.main.bind(this)); } };  /**   * @description This function is only invoked once the ResourceLoader has * successfully loaded the various required  core modules, * executing this callback on completion. This function is responsible for * assembling the relevant hook event aliases from the listing of hook names * included in  that denote the * required external dependencies and libraries required by the script. *  * @returns {void} */ init.preload = function  { // Declarations var i, n, hooks; // Definitions this.loaded = 0; hooks = Object.keys(this.Dependencies.SCRIPTS); // Assemble all hooks and attach init.load as handler for (i = 0, n = hooks.length; i < n; i++) { mw.hook(hooks[i]).add(init.load.bind(this)); } };  // Load MW modules mw.loader.using(init.Dependencies.MODULES).then(init.preload.bind(init)); // Load Dev scripts (4x) wk.importArticles({   type: "script",    articles: Object.values(init.Dependencies.SCRIPTS),  }); });

//MASS CATEGORIZATION mw.loader.using('mediawiki.api').then(function {   var groups = window.MassCategorizationGroups || ['autoconfirmed', 'bot', 'sysop'];    if ( window.MassCategorizationLoaded || !mw.config.get('wgUserGroups').some(function(el) {           return groups.indexOf(el) !== -1;        }) ) {       return;    }    window.MassCategorizationLoaded = true;    importArticle({ type: 'style', article: 'u:dev:MediaWiki:MassCategorization.css' });	var i18n = {		en: {			title: "Mass Categorization",			mode: "Mode",			add: "Add",			remove: "Remove",			replace: "Replace",			category: "Category",			categoryPlural: "Categories",			replaceWith: "Replace with",			matching: "Matching",			generalMatching: "General (does not account for piped categories)",			broadMatching: "Broad (takes care of piped links)",			noInclude: "Do not include in transclusion (for templates)",			caseSensitive: "Case sensitive (removal and replace only)",			instructions: "Put the name of each page you want to categorize on a separate line",			outputInitial: "Any errors encountered will appear below",			cancel: "Cancel",			addCategoryContents: "Add category contents",			initiate: "Initiate",			categoryPrompt: "Please enter the category name (no category prefix)",			doesNotExist: "$1 does not exist",			failedToGetContents: "Failed to get contents of $1",			categoryAlert: "Please enter at least one category", warning: "Warning", closeModalWarning: "Are you sure you want to close the modal without finishing?", close: "Close", finished: "Finished", nothingLeftToDo: "Nothing left to do, or next line is blank", noCategoryToReplace: "No $1 to replace with entered", pageNotExist: "Page $1 does not exist", addSummary: "Adding $1", addFail: "Failed to add $1 to $2", categoryAlready: "$1 already has the category $2 or an error was encountered; it has been skipped", categoryCheckFail: "Category check failed for $1; it has been skipped", removeNotFound: "$1 was not found on $2", removeFail: "Failed to remove $1 from $2", removeSuccess: "$1 successfully removed from $2", removeSummary: "Removing $1", replaceFail: "Failed to replace $1 on $2", replaceSuccess: "Category successfully replaced on $1", replaceSummary: "Replacing $1 with $2", multiReplaceSummary: "Replacing categories: $1", noCategoryReplace: "No category to replace", automatic: "automatic" }	};	//set i18n according to user's language i18n = i18n[mw.config.get("wgUserLanguage")] || i18n[mw.config.get("wgUserLanguage").split('-')[0]] || i18n.en; var categoryName = mw.config.get('wgFormattedNamespaces')[14]; var FormMC = '\  \ \           ' + i18n.mode + ': \  \ ' + i18n.add + ' \ ' + i18n.remove + ' \ ' + i18n.replace + ' \ \           \            \                + \ \                - \            \            \                ' + i18n.category + ': \  \ ' + i18n.replaceWith+ ': \  \ \               ' + i18n.matching + ': \ \               ' + i18n.generalMatching + ' \ \               <input type="radio" id="mc-broad-removal" name="mass-categorization-removal"/ value="2">' + i18n.broadMatching + ' \ \           \                <label for="no-include"><input type="checkbox" id="mc-noinclude" name="mass-categorization-noinclude"/ value="1">' + i18n.noInclude + ' \ \               <label for="case-sensitive"><input type="checkbox" id="mc-case-sensitive" name="mass-categorization-casesensitive"/ value="1">' + i18n.caseSensitive + ' \ \           \            ' + i18n.instructions + ' \ <textarea style="height: 20em; width: 80%;" id="text-categorization"/> \ \       ' + i18n.outputInitial + ' \   ',    delay = window.massCategorizationDelay || 1500, Api = new mw.Api; function click { $.showCustomModal(i18n.title, FormMC, {           id: 'form-categorization',            callback: function {                document.getElementById('mc-add-category').addEventListener('click', function(e) { e.preventDefault; document.getElementById('mc-remove-category').removeAttribute('disabled'); var container = document.getElementById('mc-categories-container'); $(container).append(' \                       ' + i18n.category + ': \                            <input type="text" class="category-name" value="" /> \                        <p class="replace-para" style="' + (document.getElementById('select-mc').value == 3 ? '' : 'display: none;') + 'padding-top: 3px;">' + i18n.replaceWith + ': \                            <input type="text" class="replace-category-name" value="" />\                    '); $(container.lastElementChild).fadeIn; });               document.getElementById('mc-remove-category').addEventListener('click', function(e) { e.preventDefault; var $toremove = $('#mc-categories-container > div:not(.removed)').last; $toremove.addClass('removed').fadeOut(400, function {                       $(this).remove;                    }); if ($toremove.parent.children(':not(.removed):last').prop('tagName') != 'DIV') this.setAttribute('disabled', 'disabled'); });               document.getElementById('select-mc').addEventListener('change', function { if (this.value == 3) { $('.replace-para').fadeIn; } else { $('.replace-para').fadeOut; }               });            },            width: 500,            buttons: [{                message: i18n.cancel,                handler: function {                    $('#form-categorization').closeModal;                }            }, {                message: i18n.addCategoryContents,                defaultButton: true,                handler: addCategoryContents            }, {                id: 'start-button',                message: i18n.initiate,                defaultButton: true,                handler: init            }]        }); }   $('#my-tools-menu').prepend(        $('<li>', { 'class': 'custom' }).append( $('', {               id: 't-mc',                text: i18n.title,                click: click            }) )   );    function logError(msg) { console.log(msg); var errBox = document.getElementById('text-error-output'); var text = document.createTextNode(msg); errBox.appendChild(text); var brTag = document.createElement('br'); errBox.appendChild(brTag); }   function addCategoryContents { var category = prompt(i18n.categoryPrompt + ' :').replace('_', ' '); Api.get({               action: 'query',                list: 'categorymembers',                cmtitle: i18n.category + ':' + category,                cmlimit: 5000,                cb: new Date.getTime            }) .done(function(d) {               if (!d.error) {                    var data = d.query;                    var pList = document.getElementById('text-categorization');                    if (data.categorymembers) {                        for (var i in data.categorymembers) {                            pList.value += data.categorymembers[i].title + '\n';                        }                    } else {                        logError(i18n.doesNotExist.replace('$1',category));                    }                } else {                    logError(i18n.failedToGetContents.replace('$1',category) + ' : ' + d.error.code);                }            }) .fail(function {               logError(i18n.failedToGetContents.replace('$1',category));            }); }   function init { var catSlots = Array.from(document.getElementsByClassName('category-name')), txt = document.getElementById('text-categorization'), pages = txt.value.split('\n'), page = pages[0], catNames = []; $.each(catSlots, function(i, name) {           name = name.value.trim;            catNames.push(name.charAt(0).toUpperCase + name.slice(1));        }); if (!catNames.filter(Boolean).length) { alert(i18n.categoryAlert); return; }       document.getElementById('start-button').setAttribute('disabled', 'disabled'); $('.blackout, #form-categorization .close').unbind; $('.blackout, #form-categorization .close').bind('click', function {           $.showCustomModal(i18n.warning, i18n.closeModalWarning, { id: 'close-warning', width: 400, buttons: [{ message: i18n.close, defaultButton: true, handler: function { // Nope, you can't close both with one call. $('#close-warning').closeModal; $('#form-categorization').closeModal; }               }, {                    message: i18n.cancel, handler: function { $('#close-warning').closeModal; }               }]            });        });        if (!page && !document.getElementById('form-complete')) { document.getElementById('start-button').removeAttribute("disabled"); $.showCustomModal(i18n.finished, i18n.nothingLeftToDo, {               id: 'form-complete',                width: 200,                callback: function {                    var $blackout = $('.blackout').last;                    $blackout.unbind;                    $blackout.click(function { $('#form-complete .wikia-button').click; });               },                buttons: [{                    message: i18n.close,                    id: 'form-complete-button',                    defaultButton: true,                    handler: function {                        $('#form-complete').closeModal;                        var $elems = $('.blackout, #form-categorization .close');                        $elems.unbind;                        $elems.click(function { $('#form-categorization').closeModal; });                   }                }]            });        } else { categorize(page, catNames); }       pages = pages.slice(1, pages.length); txt.value = pages.join('\n'); }   function categorize(pageToCat, cats) { var actionVal = document.getElementById('select-mc').value; if (actionVal == 3) { var newCatEl = Array.from(document.getElementsByClassName('replace-category-name')); var newCats = []; $.each(newCatEl, function(i, name) {               name = name.value.trim;                newCats.push(name.charAt(0).toUpperCase + name.slice(1));            }); if (!newCats.filter(Boolean).length) { alert(i18n.noCategoryToReplace.replace('$1', (newCatEl.length == 1 ? i18n.category : i18n.categoryPlural) )); document.getElementById('start-button').removeAttribute("disabled"); return; }       }        Api.get({            action: 'query',            titles: pageToCat,            prop: 'revisions|categories',            rvprop: 'content',            cb: new Date.getTime        }).done(function(d) {            var page = d.query.pages[Object.keys(d.query.pages)[0]];            var content = page.missing ===  ?  : page.revisions[0]['*'];            var newContent = content;            var config;            var summary;            if (actionVal == 1 && !d.error) {                /*                 * Add category.                 */                if (page.missing === '') {                    logError(i18n.pageNotExist.replace('$1', pageToCat));                    return;                }                var knownCats = [];                if (page.categories) {                    $.each(page.categories, function(i, categ) { knownCats.push(categ.title); });               }                var toAdd = [];                $.each(cats, function(i, cat) { if (!cat) return; cat = i18n.category + ':' + cat; if (knownCats.indexOf(cat) === -1) toAdd.push(cat); });               if (toAdd.length) {                    var sPrefix = "";                    var sSuffix = "";                    if ($('input[name=mass-categorization-noinclude]').prop('checked')) {                        sPrefix = " ";                        sSuffix = "<\/noinclude>";                    }                    summary = i18n.addSummary                        .replace('$1', (toAdd.length == 1 ? i18n.category : i18n.categoryPlural + ':') + ' ' + toAdd.join(', ') + )                        .replace(new RegExp('\\[\\[(?:' + i18n.category + '|Category):(.*?)\\]\\]', 'gi'), '$1');                    config = {                        format: 'json',                        action: 'edit',                        title: pageToCat,                        summary: summary,                        nocreate: ,                        appendtext: sPrefix + '\n' + toAdd.join('\n') + '' + sSuffix, bot: true, minor: true, token: mw.user.tokens.get('editToken') };                   $.ajax({                        url: mw.util.wikiScript('api'),                        data: config,                        dataType: 'json',                        type: 'POST',                        success: function(d) {                            if (!d.error) {                                console.log((toAdd.length == 1 ? i18n.category : i18n.categoryPlural) + ' successfully added to ' + pageToCat + '!');                            } else {                                logError(i18n.addFail.replace('$1',(toAdd.length == 1 ? i18n.category : i18n.categoryPlural )).replace('$2',pageToCat) + ': ' + d.error.code);                           }                        },                        error: function {                            logError(i18n.addFail.replace('$1',(toAdd.length == 1 ? i18n.category : i18n.categoryPlural )).replace('$2',pageToCat));                       }                    }); } else { logError(cats.length == 1 ? pageToCat + ' already has the category ' + cats[0].substring(9) + ' or an error was encountered; it has been skipped.' : pageToCat + ' has each of the categories specified; it has been skipped.'); }           } else if (actionVal == 2 && !d.error) { /*                * Remove category. */               if (page.missing === '') { logError(i18n.pageNotExist.replace('$1',pageToCat)); return; }               $.each(cats, function(i, cat) {                    if (!cat) {                        cats[i] = false;                        return;                    }                    // Remove it                    var cSens = document.getElementById('mc-case-sensitive').checked;                    var broad = document.getElementById('mc-broad-removal').checked;                    var flags = 'g' + (cSens ? '' : 'i');                   var escapedCat = $.escapeRE(cat).replace(/\s/g, '(\\s|_)');                    var escapedName = $.escapeRE(categoryName).replace(/\s/g, '(\\s|_)');                    var nRegEx = '\\[\\[' + escapedName + ':' + escapedCat + '\\]\\]';                    var sRegEx = '(\\[\\[' + escapedName + ':' + escapedCat + '\\]\\]|\\[\\[' + i18n.category + ':' + escapedCat + '\\|.*?\\]\\])';                    var regex = new RegExp(broad ? sRegEx : nRegEx, flags);                   if ($('input[name=mass-categorization-removal]:checked').val == 2) {                        regex = new RegExp(sRegEx, "gi");                    }                    if (document.getElementById('mc-noinclude').checked) {                        regex = new RegExp('\\<noinclude\\>\\s*' + (broad ? sRegEx : nRegEx) + '\\s*\\<\/noinclude\\>', flags);                    }                    if (regex.test(newContent))                         newContent = newContent.replace(regex, );                    else {                        console.log(i18n.removeNotFound.replace('$1', cat).replace('$2', pageToCat));                        cats[i] = false;                    }                    newContent = newContent.replace(regex, );                }); //don't submit if new and old contents are equal (no category found) if (newContent == content) { logError(i18n.removeNotFound.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2',pageToCat )); return; }               //submit new page cats = cats.filter(Boolean); summary = i18n.removeSummary .replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) + ' ' + i18n.category + ':' + cats.join(', ' + i18n.category + ':') + '') .replace(new RegExp('\\[\\[(?:' + i18n.category + '|Category):(.*?)\\]\\]', 'gi'), '$1'); config = { format: 'json', action: 'edit', watchlist: 'nochange', title: pageToCat, summary: summary, nocreate: '', text: newContent, bot: true, minor: true, token: mw.user.tokens.get('editToken') };               $.ajax({                    url: mw.util.wikiScript('api'),                    data: config,                    dataType: 'json',                    type: 'POST',                    success: function(d) {                        if (!d.error) {                            console.log(i18n.removeSuccess.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat) );                       } else {                            logError(i18n.removeFail.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat) + ': ' + d.error.code);                       }                    },                    error: function {                        logError(i18n.removeFail.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat));                   }                }); } else if (actionVal == 3 && !d.error) { /*                 * Replace category. */               if (page.missing === '') { logError(i18n.pageNotExist.replace('$1', pageToCat) ); return; }               // Replace it                $.each(cats, function(i, cat) {                    if (!cat) {                        console.log(i18n.noCategoryToReplace);                        cat[i] = false;                        newCats[i] = false;                        return;                    } else if (!newCats[i]) {                        console.log(cat + ': ' + i18n.noCategoryToReplace);                        cats[i] = false;                        newCats[i] = false;                        return;                    }                    var cSens = document.getElementById('mc-case-sensitive').checked;                    var broad = document.getElementById('mc-broad-removal').checked;                    var flags = 'g' + (cSens ? '' : 'i');                   var escapedCat = cat;                    ['\\', '(', ')', '[', ']', '{', '}', '?', '.', '^', '$', '|'].forEach(function(el) { escapedCat = escapedCat.replace(new RegExp('\\' + el, 'g'), '\\' + el); });                   var nRegEx = '\\[\\[' + i18n.category + ':' + escapedCat + '\\]\\]';                    var sRegEx = '(\\[\\[' + i18n.category + ':' + escapedCat + '\\]\\]|\\[\\[' + i18n.category + ':' + escapedCat + '\\|.*?\\]\\])';                    var regex = new RegExp(broad ? sRegEx : nRegEx, flags);                   var newCat = i18n.category + ':' + newCats[i].charAt(0).toUpperCase + newCats[i].substring(1);                    if (regex.test(newContent))                         newContent = newContent.replace(regex,  + newCat + );                    else {                        cats[i] = false;                        newCats[i] = false;                        return;                    }                }); // Don't submit if new and old contents are equal (no category found) if (newContent == content) { logError(i18n.removeNotFound.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat)); return; }               // Create the summary cats = cats.filter(Boolean); newCats = newCats.filter(Boolean); if (cats.length == 1) summary = i18n.replaceSummary .replace('$1',  + cats[0] + ) .replace('$2',  + newCats[0] + ); else { var replacements = ''; $.each(cats, function(i, cat) {                       var temp = replacements + '' + cat + ' → ' + newCats[i] + ', ';                        if (temp.length > 150) {                            if (replacements.indexOf('(+)') == -1)                                replacements = replacements.replace('(' + i18n.automatic + ')', '(+) (' + i18n.automatic + ')');                            return;                        }                        replacements = temp;                    }); summary = i18n.multiReplaceSummary .replace('$1', replacements.slice(0, replacements.length - 2)); }               //submit new page config = { format: 'json', action: 'edit', watchlist: 'nochange', title: pageToCat, summary: summary, nocreate: '', text: newContent, bot: true, minor: true, token: mw.user.tokens.get('editToken') };               $.ajax({                    url: mw.util.wikiScript('api'),                    data: config,                    dataType: 'json',                    type: 'POST',                    success: function(d) {                        if (!d.error) {                            console.log(i18n.replaceSuccess.replace('$1', pageToCat));                        } else {                            logError(i18n.replaceFail.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat) + ': ' + d.error.code);                       }                    },                    error: function {                        logError(i18n.replaceFail.replace('$1', (cats.length == 1 ? i18n.category : i18n.categoryPlural) ).replace('$2', pageToCat));                   }                }); } else { if (actionVal == 1) logError(i18n.categoryCheckFail.replace('$1', pageToCat) + d.error.code); else logError(i18n.failedToGetContents.replace('$1', pageToCat) + d.error.code); }       }).fail(function { if (actionVal == 1) logError(i18n.categoryCheckFail.replace('$1', pageToCat)); else logError(i18n.failedToGetContents.replace('$1', pageToCat)); });       setTimeout(init, delay);    } });

//ROLLBACK var main = { init: function { //check if user has rollback permission already var userGroups = ["rollback","content-moderator","sysop","autoconfirmed","vstf","helper","staff"], ownGroups = mw.config.get('wgUserGroups'), hasPermissionAlready = false; for (var i in ownGroups) { if (userGroups.indexOf(ownGroups[i]) !== -1) { hasPermissionAlready = true; break; }		}		if (hasPermissionAlready) return; if (mw.config.get('wgAction') == "history" && $('#pagehistory li').length > 1) $('#pagehistory li:first .mw-history-undo a').before(' rollback</a> | '); else if (mw.config.get('wgCanonicalSpecialPageName') == "Contributions") { $('#mw-content-text ul').find('li').each(function {				if ($(this).find('.mw-uctop').length)					$(this).append(' [rollback</a>] ');			}); }		else if (($.getUrlVar('diff') || $.getUrlVar('oldid')) && $('#differences-nextlink').length == 0) $('.mw-usertoollinks:last').after('   [rollback</a>] '); $('.mw-custom-rollback-link a').click(function {			main.getRevisionIdAndContent($(this).attr('data-id'));		}); },	getRevisionIdAndContent: function(title) { var API = new mw.Api; API.get({		action: 'query',		prop: 'revisions',		titles: title,		rvprop: 'user|ids',		rvlimit: 500,		cb: new Date.getTime		}) .done(function(d) {			if (!d.error) {				var revisions;				for (var i in d.query.pages) {					revisions = d.query.pages[i].revisions;				}				var currentUser = revisions[0].user, //current user rollbacking from				lastUser,				revId;				for (var i in revisions) {					if (revisions[i].user != currentUser) {						lastUser = revisions[i].user; //remember last author						revId = revisions[i].revid; //get revision to revert to						break;					}				}				if (lastUser) {					API.get({ action: 'query', prop: 'revisions', rvprop: 'content', revids: revId, cb: new Date.getTime })					.done(function(d) { if (!d.error) { var content = ""; //can be no content on page so initialise empty as failsafe for (var i in d.query.pages) { if (d.query.pages[i].revisions) content = d.query.pages[i].revisions[0]["*"]; }							main.performRollback(title,content,currentUser,lastUser); }						else new BannerNotification('Unable to rollback (failed to get page content): ' + d.error.code,'error').show; })					.fail(function { new BannerNotification('Unable to rollback: failed to get page content!','error').show; });				}				else					new BannerNotification('Unable to rollback: no different editor found!','error').show;			}			else				new BannerNotification('Unable to rollback (failed to get revisions): ' + d.error.code,'error').show;		}) .fail(function {			new BannerNotification('Unable to rollback: failed to get revisions!','error').show;		}); },	performRollback: function(page,text,user,user2) { var API = new mw.Api; API.post({		action: 'edit',		title: page,		text: text,		summary: 'Reverted edits by ' + user + ' (talk) to last version by ' + user2 + '',		token: mw.user.tokens.values.editToken		}) .done(function(d) {			if (!d.error) {				new BannerNotification('Rollback successful!','confirm').show;							}			else				new BannerNotification('Unable to rollback (failed to publish edit): ' + d.error.code,'error').show;		}) .fail(function {			new BannerNotification('Unable to rollback: failed to publish edit!','error').show;		}); } }; main.init; }) (this.jQuery, this.mediaWiki);
 * (function($, mw) {