User:Gossipjanet/common.js

/** * * MassEdit.js * @file Essentially "bot software lite"; task automation and bulk editing tool * @author Eizen  * @license CC-BY-SA 3.0 * @external "ext.wikia.LinkSuggest" * @external "mediawiki.user" * @external "mediawiki.util" * @external "I18n-js" * @external "Modal.js" * @external "Placement.js" * @external "WgMessageWallsExist.js" */ /** * * Table of contents        Summary * - Prototype/Setup namespaces     Namespace object declarations/definitions * - Prototype pseudo-enums         Storage for MassEdit utility constants * - Setup pseudo-enums             Storage for   constants * - Prototype Utility methods      General purpose helper functions * - Prototype Dynamic Timer        Custom   iterator * - Prototype Quicksort            Fast Quicksort algorithm for member pages * - Prototype API methods          Assorted GET/POST handlers and page listers * - Prototype Generator methods    Functionality methods to build page lists * - Prototype Assembly methods     Methods returning   HTML * - Prototype Modal methods        All methods related to displaying interface *  - Utility methods               General modal-specific helper functions *  - Preview methods               Methods related to the preview pseudo-scene *  - Modal methods                 More general modal builders, handlers, etc. * - Prototype Event handlers        Handlers for clicks of modal buttons * - Prototype Pseudo-constructor   MassEdit   method * - Setup Helper methods           Methods to validate user input * - Setup Primary methods          Methods to load external dependencies * */ /* jshint -W030, undef: true, unused: true, eqnull: true, laxbreak: true, bitwise: false */ "use strict"; // Prevent double loads and respect prior double load check formatting if (!window || !$ || !mw || module.isLoaded || window.isMassEditLoaded) { return; } module.isLoaded = true; /****************************************************************************/ /*                       Prototype/Setup namespaces                         */ /****************************************************************************/ /**   * @description All script functionality is contained in a pair of namespace * objects housed in the module-global scope. Originally declared as ES2015 * s due to JSMinPlus treating the keyword as permissible, * the namespaces were subsequently redeclared with  for the * purposes of ensuring ES5 consistency/compatibility. */ var main, init; /**  * @description The   namespace object is used as a class * prototype for the MassEdit class instance created by. It  * contains methods and properties related to the actual MassEdit * functionality and application logic, keeping in a separate object all the * methods used to load and initialize the script itself. */ main = {}; /**  * @description The   namespace object contains methods and * properties related to the setup/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 script setup is complete. */ init = {}; /****************************************************************************/ /*                         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_MATCH: "massedit-content-match", 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",          "content-moderator",          "bot",          "bot-global",          "staff",          "soap",          "helper",          "vanguard",          "wiki-manager",          "content-team-member",          "content-volunteer",        ]), CAN_MESSAGE: Object.freeze([         "global-discussions-moderator",          "threadmoderator",        ]), }),   },    /**     * @description The   pseudo-enum is used to store data     * and names related to building the four major operations supported by the     * MassEdit script, namely find-and-replace, append/prepend content, message     * users, and generation of page listings. Originally, this enum was an     * array used to store the   names of the four main scenes     * in the order that determined their placement in the associated operation     * dropdown menu. The actual design schema used to dynamically build the     *   HTML from builder functions was stored within a     * since-removed method called   that built     * all the scenes at once on program initialization.     *
 * (function (module, window, $, mw) {

*

* However, with the decision to revise this messy approach in favor of    * lazy-building scenes only when requested by the user, the scene schema * was moved to this enum and rearranged into  form. The * author was forced to make use of many nested * invocations due to the inability to deep-freeze the whole object, though * alternate approaches are being researched to de-uglify the enum in a    * future update. *    * @readonly * @enum {object} */   Scenes: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       // Find-and-replace (1st scene, default)        REPLACE: Object.freeze({ NAME: "replace", POSITION: 0, SCHEMA: Object.freeze([           Object.freeze({ HANDLER: "assembleDropdown", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["type", Object.freeze(["pages", "categories", "namespaces"]) ]),               Object.freeze(["case", Object.freeze(["sensitive", "insensitive"]) ]),               Object.freeze(["match", Object.freeze(["plain", "regex"]) ]),             ])            }),            Object.freeze({ HANDLER: "assembleTextfield", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["target", "textarea"]),                Object.freeze(["indices", "input"]),                Object.freeze(["content", "textarea"]),                Object.freeze(["pages", "textarea"]),                Object.freeze(["summary", "input"]),              ]) })         ]),        }),        // Append/prepend content (2nd scene)        ADD: Object.freeze({ NAME: "add", POSITION: 1, SCHEMA: Object.freeze([           Object.freeze({ HANDLER: "assembleDropdown", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["action", Object.freeze(["prepend", "append"]) ]),               Object.freeze(["type", Object.freeze(["pages", "categories", "namespaces"]) ])             ])            }),            Object.freeze({ HANDLER: "assembleTextfield", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["content", "textarea"]),                Object.freeze(["pages", "textarea"]),                Object.freeze(["summary", "input"]),              ]) })         ]),        }),        // Mass-message users (3rd scene)        MESSAGE: Object.freeze({ NAME: "message", POSITION: 2, SCHEMA: Object.freeze([           Object.freeze({ HANDLER: "assembleTextfield", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["pages", "textarea"]),                Object.freeze(["byline", "input"]),                Object.freeze(["body", "textarea"]),              ]) })         ]),        }),        // List cat/ns members (4th scene)        LIST: Object.freeze({ NAME: "list", POSITION: 3, SCHEMA: Object.freeze([           Object.freeze({ HANDLER: "assembleDropdown", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["type", Object.freeze(["categories", "namespaces", "templates"]) ])             ])            }),            Object.freeze({ HANDLER: "assembleTextfield", PARAMETER_ARRAYS: Object.freeze([               Object.freeze(["pages", "textarea"]),                Object.freeze(["members", "textarea"]),              ]) })         ]),        }),      }),    },    /**     * @description This pseudo-enum replaces the previous pair of     *   constants housed in the script-global execution * context, namely  and. This enum * houses the default values of these flags, the value of which are applied * to the properties of the MassEdit instance's  local * variable. This system allows for the exposing of certain public methods * that permit post-load toggling of the debug and test modes for more * dynamic debugging. *    * @readonly * @enum {boolean} */   Flags: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       DEBUG: false,        TESTING: false,      }), },   /**     * @description The   pseudo-enum of the *  namespace object is used to store various constants for * general use in a variety of contexts. The constants of the *  data type are related to standardizing 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. *

*

* The two  data type members are the key name used to     * store HTML "scenes" (operation interfaces) in the browser's     *   and the name of the "scene" serving as the * first interface built and displayed to the user upon initialization. By    * convention, this is the "Find and replace" scene, though any scene could * have been used. *    * @readonly * @enum {string|number} */   Utility: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       LS_KEY: "MassEdit-cache-scenes",        FIRST_SCENE: "REPLACE",        MAX_SUMMARY_CHARS: 800,        FADE_INTERVAL: 1000,        DELAY: 35000,      }), }, });  /****************************************************************************/  /*                           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 "ARTICLES," * originally contained key/value pairs wherein the key was the specific * name of the  and the value was the script's location * for use by. However, this system * was eventually replaced in favor of an array of s     * containing properties for hook,   alias, and script * for more efficient, readable loading of dependencies. *

*

* The latter array, a constant array named, contains a     * listing of the core modules required for use by     *. It may be worth noting for future reference * that  doesn't exist yet in the UCP, so     * an error will be thrown somewhere if the script is loaded on a UCP wiki. *

*

* The key for the  array entries is as follows: *     * - DEV/WINDOW: The name and location of the   property * - HOOK: The name of the  event * - ARTICLE: The location of the script or stylesheet on the Dev wiki * - TYPE: Either "script" for JS scripts or "style" for CSS stylesheets *     *     * @readonly * @enum {object} */   Dependencies: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       ARTICLES: Object.freeze([ Object.freeze({           DEV: "i18n",            HOOK: "dev.i18n",            ARTICLE: "u:dev:MediaWiki:I18n-js/code.js",            TYPE: "script",          }), Object.freeze({           DEV: "modal",            HOOK: "dev.modal",            ARTICLE: "u:dev:MediaWiki:Modal.js",            TYPE: "script",          }), Object.freeze({           DEV: "placement",            HOOK: "dev.placement",            ARTICLE: "u:dev:MediaWiki:Placement.js",            TYPE: "script",          }), Object.freeze({           WINDOW: "wgMessageWallsExist",            HOOK: "dev.enablewallext",            ARTICLE: "u:dev:MediaWiki:WgMessageWallsExist.js",            TYPE: "script",          }), ]),       MODULES: Object.freeze([ "ext.wikia.LinkSuggest", "mediawiki.user", "mediawiki.util", ]),     }),    },    /**     * @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 pseudo-enum is used to store the * names of the various  (wg) variables required * by the main MassEdit class instance and  object. These * are fetched within the body of the  function via * a  invocation and stored in an instance * variable property named  for subsequent usage. This * approach replaces the deprecated approach previously used in the script * of assuming the relevant wg variables exist as properties of the *  object, an assumption that is discouraged in more * recent version of MediaWiki. *    * @readonly * @enum {object} */   Globals: { enumerable: true, writable: false, configurable: false, value: Object.freeze([       "wgFormattedNamespaces",        "wgLegalTitleChars",        "wgLoadScript",        "wgUserGroups",        "wgVersion",      ]), },   /**     * @description This catchall pseudo-enum of the   constant denoting the * name of the script and another  for the name of the *  event. *    * @see SUS-4775 * @see VariablesBase.php * @readonly * @enum {string|number} */   Utility: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       SCRIPT: "MassEdit",        HOOK_NAME: "dev.massEdit",        STD_INTERVAL: 1500,        BOT_INTERVAL: 750,        CACHE_VERSION: 3,      }), } });  /****************************************************************************/  /*                      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("^[" + this.globals.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(this.globals.wgUserGroups.join(" ")) || this.flags.testing; }; /**   * @description This helper method serves as the primary means by which all * external post-load toggling of the debug and test modes may be undertaken. * This particular implementation makes use of the bitwise XOR operator to  * toggle the   representation of the flag's   *   data type. *  * @param {string} paramFlagName -   name of desired flag * @returns {void} */ main.toggleFlag = function (paramFlagName) { if (     typeof paramFlagName !== "string" ||      $.inArray(paramFlagName.toUpperCase, Object.keys(this.Flags)) === -1    ) { return; }   // Check for boolean flag's existence and add default if undefined (this.flags = this.flags || {})[paramFlagName] = this.flags[paramFlagName] || this.Flags[paramFlagName]; // Toggle via bitwise then type coerce back to boolean before redefining window.console.log(paramFlagName + ":",     this.flags[paramFlagName] = !!(this.flags[paramFlagName] ^= 1)); }; /**   * @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 Originally, this function returned an ammended string adjusted * to exhibit the user's desired content changes. The function was called for * every individual page or category/namespace member inputted by the user. * The author eventually noticed that all but one of the input arguments * passed were unchanged from invocation to invocation and that the same * internal operations were being undertaken and performed each time despite * the unchanged arguments. *

*

* As an improvement, the author refactored the method to use a closure, * allowing the main outer function to be called only once to initialize * internal fields with the unchanged arguments. Thanks to the closure, the * returned inner function can be assigned to a local variable elsewhere and * invoked as many times as needed while still making use of preserved closure * variables housed in heap memory after the main function's frame is popped * off the stack. *  * @param {boolean} paramIsCaseSensitive - If case sensitivity is desired * @param {boolean} paramIsUserRegex - If user has input own regex * @param {string} paramTarget - Text to be replaced * @param {string} paramReplacement - Text to be inserted * @param {Array } paramInstances - Indices at which to replace text * @returns {function} - A closure function to be invoked separately */ main.replaceOccurrences = function (paramIsCaseSensitive, paramIsUserRegex,      paramTarget, paramReplacement, paramInstances) { // Declarations var regex, replacement, counter; // Sanitize input param paramInstances = (paramInstances != null) ? paramInstances : []; // First parameter of the String.prototype.replace invocation regex = new RegExp((paramIsUserRegex)     ? paramTarget // Example formatting: ([A-Z])\w+      : paramTarget        .replace(/\r/gi, "")        .replace(/([.*+?^=!:${}|\[\]\/\\])/g, "\\$1"),      ((paramIsCaseSensitive) ? "g" : "gi") + "m"   ); // Second parameter of the String.prototype.replace invocation replacement = (!paramInstances.length) ? paramReplacement : function (paramMatch) { return ($.inArray(++counter, paramInstances) !== -1) ? paramReplacement : paramMatch; };   // Closure so above operations are only undertaken once per submission op    return function (paramString) { // Log regex and intended replacement if (this.flags.debug) { window.console.log(regex, replacement); }     // Init counter in case of replace function counter = 0; // Replace using regex and either paramReplacement or anon function return paramString.replace(regex, replacement); }.bind(this); }; /**   * @description This (admittedly messy) handler is used for both returning * scene data from storage and for adding new scene data to storage for reuse. *  is accessed safely via the jQuery store plugin *  and placed within a   block to   * handle any additional thrown errors not handled by. A  * local object stored in   is used as a fallback in the * event of an error being thrown. *  * @see Wikia's jquery.store.js * @param {string} paramSceneName - Name for requested scene * @param {string} paramSceneData - Content of scene for setting (optional) * @returns {string|null} - Returns scene content or    */ main.queryStorage = function (paramSceneName, paramSceneData) { // Declarations var isSetting, scenes; // Handler can be used for both getting and setting, so check for which isSetting = (Array.prototype.slice.call(arguments).length == 2 &&     paramSceneData != null); // Apply localStorage data to this.modal.scenes and local scenes variable try { scenes = this.modal.scenes = $.storage.get(this.Utility.LS_KEY) || {}; } catch (paramError) { if (this.flags.debug) { window.console.error(paramError); }     // Use fallback if localStorage throws scenes = this.modal.scenes = this.modal.scenes || {}; }   // Return string HTML of requested scene or explicit null if (!isSetting) { return (scenes.hasOwnProperty(paramSceneName)) ? scenes[paramSceneName] : null; }   // Add to storage if no property with this name exists if (!scenes.hasOwnProperty(paramSceneName)) { // Simultaneously adds to both scenes variable and this.modal.scenes scenes[paramSceneName] = paramSceneData; // Add to localStorage try { $.storage.set(this.Utility.LS_KEY, scenes); } catch (paramError) {} // Make sure new scenes are added to both localStorage and modal.scenes if (this.flags.debug) { try { window.console.log("modal.scenes: ", this.modal.scenes); window.console.log("localStorage: ",           JSON.parse(window.localStorage.getItem(this.Utility.LS_KEY))); } catch (paramError) {} }   }    return scenes[paramSceneName]; }; /****************************************************************************/  /*                       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; window.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; window.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 = window.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 $.nirvana.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 $.nirvana.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 $.nirvana.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", minor: true, bot: true, format: "json", }, paramConfig)   }); }; /****************************************************************************/  /*                    Prototype Generator methods                           */ /****************************************************************************/ /**   * @description Originally a part of the   function, * this method is used to return a  object that passes * back either an error message for display in the modal status log or an  * array containing wellformed titles of individual loose pages, categories, * or namespaces. If the type of entries contained with the parameter array is  * either pages, usernames, or categories, the function checks that their * titles are comprised of legal characters. If the type is namespace, it  * checks that the number passed is a legitimate integer. It also prepends the * appropriate namespace prefix as applicable as denoted in  *. *

*

* The function returns a  promise instead of an array * due to the function's use in conjunction with * in the body of  and due to the desire to only * permit handlers to add log entries and adjust the view. Originally, this * function itself added log entries, which the author felt should be the sole * responsibility of the handlers attached to user-facing modal buttons rather * than helper functions like this and. *  * @param {Array } paramEntries - Array of pages/cats/ns * @param {string} paramType - categories, templates, namespaces, recipients * @returns {object} $deferred - Promise returned for use w/    */ 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 = this.globals.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 (       paramType === "recipients" &&        this.startsWith(entry, this.globals.wgFormattedNamespaces[2])      ) { entry = entry.split(this.globals.wgFormattedNamespaces[2] + ":")[1]; }     // 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; }; /**   * @description This method is used during the user messaging operation to   * ensure that the user accounts being messaged actually exist so as to avoid * the intentional or unintentional addition of messages to the walls/talk * pages of nonexistant users. Future updates to this functionality may * eventually include checks for users with edit counts of 0, indicating that * the user in question exists but does not contribute to the wiki in on  * which the script is being used. In such cases, perhaps the username will be  * removed. *  * @param {Array } paramEntries - Array of usernames to check * @returns {object} - $.Deferred promise object */ 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 = this.globals.wgFormattedNamespaces[ (this.info.hasMessageWalls) ? 1200       : 3    ] + ":";    // "User talk:" userPrefix = this.globals.wgFormattedNamespaces[3] + ":"; // Array of extant usernames with prefix names = []; entries = []; // Get wellformed, formatted usernames $getUsers = this.getValidatedEntries(paramEntries, "recipients"); // Once acquired, apply to names array or pass along rejection message $getUsers.then(function (paramResults) {     names = paramResults;    }, $returnUsers.reject.bind($)); // Log paramResults if (this.flags.debug) { window.console.log(names); }   // Indicate checking is in progress $returnUsers.notify("logStatusCheckingUsernames"); // Iterate over provided list of usernames this.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.config.interval); /**    * @description For each username of   that is     * checked, the  's   handler is     * invoked to check the status of the username and determine if the related * account actually exists. The status is then displayed to the user by    * means of a status log message passed to   via * .     */    $addUser.progress(function (paramResults, paramStatus, paramXHR) {      if (this.flags.debug) {        window.console.log(paramResults, paramStatus, paramXHR);      }      if (paramStatus !== "success" || paramXHR.status !== 200) {        $returnUsers.notify("logErrorNoUserData", names[counter++]);        return this.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.timer.iterate;    }.bind(this)); return $returnUsers.promise; }; /**   * @description This function is used to return a jQuery * object providing a  or   invocation with * an array of wellformed pages for editing. It accepts as input an array * containing titles of either categories, namespaces, or templates from which * to acquire member pages or transclusions. In such cases, a number of API * calls are made requesting the relevant members contained in the input * categories or namespaces. These are checked and pushed into an entries * array. Once complete, the entries array is returned by means of a resolved * .   *

*

* Originally, this function also served to validate loose pages passed in the * parameter array, running them against the legl characters and returning the *  array for use. However, per the single responsibility * principle, this functionality was eventually removed into a separate method * called  that is called by this method to   * ensure that the category/namespace titles are wellformed prior to making * API queries. *  * @param {Array } paramEntries - Array of user input pages * @param {string} paramType -  denoting cat, ns, or tl   * @returns {object} $returnPages - $.Deferred promise object */ 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.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.config.interval); /**    * @description Once the member pages from the specific category or     * namespace have been returned following a successful API query, the * $addPages  is notified, allowing for this callback * function to sanitize the returned data and push the wellformed member * page titles into the  array. If there are still * remaining pages as indicated by a "query-continue" property, the counter * is left unincremented and the relevant continuer parameter added to the *  object. In any case, the function ends with a    * call to iterate the timer. */   $addPages.progress(function (paramResults, paramStatus, paramXHR) {      if (this.flags.debug) {        window.console.log(paramResults, paramStatus, paramXHR);      }      if (paramStatus !== "success" || paramXHR.status !== 200) {        $returnPages.notify("logErrorFailedFetch", names[counter++]);        return this.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.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.timer.iterate;    }.bind(this)); return $returnPages.promise; }; /****************************************************************************/  /*                      Prototype Assembly methods                          */ /****************************************************************************/ /**   * @description This function is a simple recursive   HTML * generator that makes use of 's assembly methods to   * construct wellformed HTML strings from a set of nested input arrays. This * allows for a more readable means of producing proper HTML than the default *  approach or the hardcoded HTML * approach employed in earlier iterations of this script. Through the use of  * nested arrays, this function permits the laying out of parent/child DOM * nodes in array form in a fashion similar to actual HTML, enhancing both * readability and usability. *

*

* Furthermore, as the  function returns a   * , nested invocations of the method within parameter * arrays is permitted, as evidenced in certain, more specialized assembly * methods elsewhere in the script. *

*

* An example of wellformed input is shown below: *

*   * this.assembleElement(   *   ["div", {id: "foo-id", class: "foo-class"},   *     ["button", {id: "bar-id", class: "bar-class"},   *       "Button text",   *     ],   *     ["li", {class: "overflow"},   *       ["a", {href: "#"},   *         "Link text",   *       ],   *     ],   *   ],   * ); *   *   * @param {Array } paramArray - Wellformed array representing DOM nodes * @returns {string} - Assembled  HTML */ 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)); }; /**   * @description This specialized assembly function is used to create a tool * link to inclusion at the location specified via the * instance property. Like the  toolbar button on which * it is based, the element (in  form) returned from this * function constitutes a link element enclosed within a list element. *  * @param {string} paramText - Title/item text * @returns {string} - Assembled  HTML */ 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,        ],      ]    ); }; /**   * @description This function is one of two similar specialized assembly * functions used to automate the construction of several reoccuring * components in the modal content body. This function builds two types of  * textfield, namely  s and  s. The * components may be disabled at creation via parameter. * The function also automatically assembles element selector names and * I18n message titles as needed. *  * @param {string} paramName - Name for message, id/classname generation * @param {string} paramType -  or     * @returns {string} - Assembled   HTML */ 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],      ]    ); }; /**   * @description This function is one of two similar specialized assembly * functions used to automate the construction of several reoccuring * components in the modal content body. This function is used to build * dropdown menus from a default value and an array of required * s. As with , it also * assembles element selector names and I18n message names for all elements. * Per a recent update, the default dropdown option has been removed in favor * of a default option denoted by the  parameter. *  * @param {string} paramName -   name of the dropdown * @param {Array } paramValues - Array of dropdown options * @param {number} paramIndex - Optional selected index * @returns {string} - Assembled  HTML */ 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 = window.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 /**  * @description This modal helper function is used simply to inject modal * styling prior to the creation of the new  instance. It is  * used to style scene-specific elements as well as the messaging preview * pseudo-scene displayed when the user attempts to parse the message content. * While the styles could be stored in a separate, dedicated *  file on Dev, their inclusion here * allows for fast adjustment of selector names without the hassle of editing * the contents of multiple files. due to the use of a    * object collating all ids and classes evidenced in the modal in a single * place. *  * @returns {void} */ 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.timer && !this.timer.isComplete) { this.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); }; /**   * @description This helper function is used to disable certain elements and * enable others depending on the operation being performed. It is used * primarily during editing to disable one of several element groups related * to either replace fields or the fieldset/modal buttons in order to prevent * illegitimate mid-edit changes to input. If the fieldset, etc. is disabled, * the method enables the buttons related to pausing and canceling the editing * operation, and vice versa. Likewise, the preview button is only displayed * when the messaging scene is being viewed, and only when the editing * operation is not running. *  * @param {boolean} paramValue - Whether or not the form/fieldset is disabled * @returns {void} */ 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, previewScene, 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; }   // See if the preview scene has been saved to storage previewScene = this.queryStorage("preview"); // Preview modal scene contents contents = ((previewScene != null)     ? previewScene      : this.queryStorage("preview", this.buildPreviewContent)    ).replace("$1", $byline).replace("$2", paramBody); // 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 In its initial incarnation under the original title of   * , this function was used to assemble all * four main so-called "scenes" that make up the body content of the *  instance and serve as the user interfaces for the * associated operations. However, the method was inherently inefficient for * building all four scenes every time MassEdit was initialized by the user. * Though the scenes were temporarily cached in a local *  storage , they were not * added to  and thus were rebuilt every time the * user navigated away from the page on which MassEdit was loaded. *

*

* To fix this inefficiency and improve the process, the author eventually * replaced this approach with a lazy-load-style system that only builds * scenes as they are needed and stores preassembled scenes in the browser *  object via   for subsequent * reuse. To accomplish this, this function was stripped down and rewritten. * Under its present design, the function first checks storage to see if the * scene has already been built, returning the scene from storage if it has * been assembled before. Otherwise, the method builds the string HTML from * design schema housed in, adds that HTML to   * storage, and returns the result. *  * @param {string} paramScene - Name of the desired scene to build * @returns {string} - Assembled string HTML of the desired scene */ main.buildModalScene = function (paramScene) { // Declarations var i, j, m, n, tempScene, sceneNames, assembledScene, framework, enumScene, dropdownArgs, elements, enumSchema, enumArrays, selector, storedResults; // See if a copy of this scene already exists in storage storedResults = this.queryStorage(paramScene); // If the scene exists in storage, return that copy if (storedResults != null && storedResults.length) { return storedResults; }   // Basic modal form framework framework = this.buildModalContent; // Grab scene config object associated with input argument string enumScene = this.Scenes[paramScene.toUpperCase]; // Temporary array for scene names used to construct dropdown options sceneNames = []; // Better than Object.keys(this.Scenes) since key names could change for (tempScene in this.Scenes) { if (this.Scenes.hasOwnProperty(tempScene)) { sceneNames.push(this.Scenes[tempScene].NAME); }   }    // Make copy of defaults and add the index dropdownArgs = ["scene", sceneNames, enumScene.POSITION]; // New defaultArgs object logged if (this.flags.debug) { window.console.log(dropdownArgs); }   // Init string HTML elements = ""; // ID selector selector = "ID_CONTENT_" + enumScene.NAME.toUpperCase; // Use schema to dynamically construct string HTML from builder functions for (i = 0, n = enumScene.SCHEMA.length; i < n; i++) { enumSchema = enumScene.SCHEMA[i]; enumArrays = enumSchema.PARAMETER_ARRAYS; for (j = 0, m = enumArrays.length; j < m; j++) { elements += this[enumSchema.HANDLER].apply(this, enumArrays[j]); }   }    // Make use of modal framework to insert scene-specific HTML assembledScene = framework .replace("$1", this.Selectors[selector]) .replace("$2", this.assembleDropdown.apply(this, dropdownArgs)) .replace("$3", elements); // Add this scene to storage for subsequent usage this.queryStorage(paramScene, assembledScene); // Return scene HTML return assembledScene; }; /**   * @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 window.dev.modal.Modal({     content: this.buildModalScene(this.Scenes[this.Utility.FIRST_SCENE].NAME),      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 (this.flags.debug) { window.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.timer && !this.timer.isComplete) { if (this.flags.debug) { window.console.dir(this.timer); }     return; }   // Declarations var $action, $type, $case, $match, $content, $target, $indices, indices, $pages, pages, $byline, $summary, counter, config, data, pageIndex, newText, $getPages, $postPages, $getNextPage, $getPageContent, $postPageContent, error, $scene, isCaseSensitive, isUserRegex, isReplace, isAddition, isMessaging, isListing, $members, $selected, $body, pagesType, replaceOccurrences; // 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]; $match = $("#" + this.Selectors.ID_CONTENT_MATCH)[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 window.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"); // Whether user has input regex for finding & replacing isUserRegex = ($match.selectedIndex === 1 &&       $match.value === "regex"); // Define regex, etc. only once per submission operation using closure replaceOccurrences = this.replaceOccurrences(isCaseSensitive,       isUserRegex, $target, $content, indices); // Check closure scope's variables under Scopes if (this.flags.debug) { window.console.dir(replaceOccurrences); }   }    // 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 (this.flags.debug) { window.console.log("hasMessageWalls: ", this.info.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.info.hasMessageWalls != null) ? $paramInner.resolve.promise : window.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.info.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 (this.flags.debug) {        window.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.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.config.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 (this.flags.debug) {        window.console.log("$postPages results: ", paramResults);      }      // Addition parameters      if (isAddition) {        config = {          handler: "postPageContent",          parameters: {            title: pages[counter],            token: mw.user.tokens.get("editToken"),            summary: $summary,          }        };        if (!this.flags.testing) {          // "appendtext" or "prependtext"          config.parameters[$action.value.toLowerCase + "text"] = $content;        }      // Find-and-replace parameters      } else if (isReplace) {        // Make sure returned results have a "query" property        if ( paramResults.query == null || !paramResults.hasOwnProperty("query") ) {         this.addModalLogEntry("logErrorEditing", pages[counter++]);          return this.timer.iterate;        }        pageIndex = Object.keys(paramResults.query.pages)[0];        data = paramResults.query.pages[pageIndex];        // Shim to handle ArticleComments that do not have revision history        if ( !data.hasOwnProperty("revisions") || !this.isThisAn("Array", data.revisions) ) {         this.addModalLogEntry("logErrorEditing", pages[counter++]);          return this.timer.iterate;        }        // Return if page doesn't exist to the server        if (pageIndex === "-1") {          this.addModalLogEntry("logErrorNoSuchPage", pages[counter++]);          return this.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 = replaceOccurrences(config.parameters.text);        // 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.timer.iterate; } else { if (!this.flags.testing) { config.parameters.text = newText; }       }      // Messaging parameters } else if (isMessaging) { config = [ {           handler: "postTalkPageTopic", parameters: (this.flags.testing) ? {} : {             sectiontitle: $byline, text: $body, title: pages[counter], }         },          {            handler: "postMessageWallThread", parameters: (this.flags.testing) ? {} : {             messagetitle: $byline, body: $body, pagetitle: pages[counter], }         },        ][+this.info.hasMessageWalls]; }     // Log all config handlers and parameters if (this.flags.debug) { window.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 (this.flags.debug) { window.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.info.hasMessageWalls)) && paramData.edit && paramData.edit.result === "Success" ) ||       (          isMessaging && this.info.hasMessageWalls && paramData.status && paramData.statusText !== "error" )     ) {        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.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.timer && !this.timer.isComplete) { if (this.flags.debug) { window.console.dir(this.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.info.hasMessageWalls != null) ? $paramInner.resolve(this.info.hasMessageWalls).promise : window.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.info.hasMessageWalls == null) { this.info.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 (this.flags.debug) {        window.console.log("$previewMessage results: ", paramResults);      }      // Bypass handleClear's default functionality via the functions object      this.handleClear({ before: this.displayPreview.bind(this, paramResults[         (this.info.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.timer ||      (this.timer && this.timer.isComplete)    ) { if (this.flags.debug) { window.console.dir(this.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.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.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.timer ||      (this.timer && this.timer.isComplete)    ) { if (this.flags.debug) { window.console.dir(this.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.timer && !this.timer.isComplete) { if (this.flags.debug) { window.console.dir(this.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.buildModalScene($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   properties * and defines the toolbar element and its associated event listener, namely * . The method also populates and returns an   *   containing methods and aliases for addition to   * , a container for public, exposed * methods externally accessible for post-load debugging purposes. *

*

* Following this function's invocation, the MassEdit class instance will have * a total of seven instance variables, namely, , *,  ,  ,   *  ,  , and. All other * functionality related to MassEdit is stored in the class instance * prototype, the  namespace object, for convenience. *  * @returns {object} exports - Methods exposed via */ main.init = function  { // Declarations var i, n, $toolItem, toolText, exports, flags, flag, publicMethod; // Definitions exports = {}; flags = Object.keys(this.Flags); // I18n config for wiki's content language this.i18n.useContentLang; // Initialize new modal property this.modal = {}; // Initialize a new dynamic timer object this.timer = null; // Set default null placeholder value this.info.hasMessageWalls = null; // Replacement for previous script-global constants; apply default values this.flags = { debug: this.Flags.DEBUG, testing: this.Flags.TESTING, };   // 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.config.placement.element)[this.config.placement.type]($toolItem); // Assemble MassEdit instance's public methods for module.exports for (i = 0, n = flags.length; i < n; i++) { flag = flags[i].toLowerCase; publicMethod = "toggle" + this.capitalize(flag); exports[publicMethod] = this.toggleFlag.bind(this, flag); }   // Return public methods to be added to module.exports object return exports; }; /****************************************************************************/  /*                         Setup Helper methods                             */ /****************************************************************************/ /**   * @description This helper function is used to automatically generate an   * appropriate contrived ResourceLoader module name for use in loading scripts * via  on UCP wikis. The use of this function * replaces the previous approach that saw the inclusion of hardcoded module * names as properties of the relevant dependency s stored * in. When passed an argument * formatted as, the function will * extract the subdomain name ("dev") and join it to the name of the script * ("Test") with the article type ("script") as. *  * @param {string} paramType - Either "script" or "style" * @param {string} paramPage - Article formatted as "u:dev:MediaWiki:Test.js" * @returns {string} - ResourceLoader module name formatted "script.dev.Test" */ init.generateModuleName = function (paramType, paramPage) { return $.merge([paramType], paramPage.split(/[\/.]+/)[0].split(":").filter( function (paramItem) { return !paramItem.match(/^u$|^mediawiki$/gi); }   )).join("."); }; /**   * @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} validatedConfig - Adjusted Placement.js config */ init.definePlacement = function (paramConfig) { // Declarations var validatedConfig, loader; // Definitions validatedConfig = {}; loader = window.dev.placement.loader; try { validatedConfig.element = loader.element(paramConfig.element); } catch (e) { validatedConfig.element = loader.element(this.Placement.DEFAULTS.ELEMENT); }   try { validatedConfig.type = loader.type(       (this.Placement.VALID_TYPES.indexOf(paramConfig.type) !== -1)          ? paramConfig.type          : this.Placement.DEFAULTS.TYPE      ); } catch (e) { validatedConfig.type = loader.type(this.Placement.DEFAULTS.TYPE); }   // Set script name loader.script(this.Utility.SCRIPT); return Object.freeze(validatedConfig); }; /**   * @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) { // Declaration var isNumber; // Definition isNumber = (typeof value === "number" && window.isFinite(paramInterval) &&     !window.isNaN(paramInterval)); if (     this.globals.wgUserGroups.indexOf("bot") !== -1 &&      (paramInterval < this.Utility.BOT_INTERVAL || !isNumber)    ) { return this.Utility.BOT_INTERVAL; // Reset to max 80 edits/minute } else if (     this.globals.wgUserGroups.indexOf("user") !== -1 &&      (paramInterval < this.Utility.STD_INTERVAL || !isNumber)    ) { return this.Utility.STD_INTERVAL; // Reset to max 40 edits/minute } else { return window.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. Once the new instance is  * created and added as a property of the   object, the method * creates a new protected property of  called *  used to store exposed public methods for use in   * post-load debugging. At the method's end, a  is fired * for potential coordination with other scripts or user code. *

*

* 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 {undefined|function} paramRequire - Via * @param {object} paramLang - i18n  returned from hook * @returns {void} */ init.main = function (paramRequire, paramLang) { // Declarations var i, n, configTypes, descriptor, parameter, lowercase, method, property, descriptorProperties, userConfig, field, instanceFields, initExports, instanceExports; // Cleanup init namespace by deleting temp variables delete this.modules; // Two types of config object property configTypes = ["Interval", "Placement"]; // MassEdit object instance's local fields and values instanceFields = [ {       name: "i18n", value: paramLang, },     {        name: "config", value: {}, },     {        name: "info", value: $.extend({}, this.info), },     {        name: "globals", value: $.extend({}, this.globals), },   ];    // Support both MassEdit config and legacy Message config userConfig = window.MassEditConfig || window.configMessage || {}; // New Object.create descriptor object descriptor = {}; // Default descriptor access properties descriptorProperties = { enumerable: true, configurable: false, writable: false, };   // Assemble new descriptor entries to serve as instance local data for (i = 0, n = instanceFields.length; i < n; i++) { field = instanceFields[i]; descriptor[field.name] = $.extend({}, descriptorProperties); descriptor[field.name].value = field.value; }   // Define and validate user config input for (i = 0, n = configTypes.length; i < n; i++) { // Definitions property = configTypes[i]; method = "define" + property; lowercase = property.toLowerCase; parameter = (userConfig.hasOwnProperty(lowercase)) ? userConfig[lowercase] : null; // Define descriptor property value Object.defineProperty(descriptor.config.value, lowercase,       $.extend($.extend({}, descriptorProperties), { value: this[method](parameter), })     );    }    // Public methods for init object initExports = { observeScript: window.console.dir.bind(this, this), observeUserConfig: window.console.dir.bind(this, userConfig), };   // Create MassEdit instance, keep for future observation, and store exports instanceExports = (this.instance = Object.create(main, descriptor)).init; // Once instance is created, expose public methods for external debugging Object.defineProperty(module, "exports",     $.extend($.extend({}, descriptorProperties), { value: Object.freeze($.extend(initExports, instanceExports)), })   );    // Dispatch hook with window.dev.massEdit once initialization is complete mw.hook(this.Utility.HOOK_NAME).fire(module); }; /**   * @description Originally a pair of functions called * and, this function is used to load all required * external dependencies from Dev and attach  listeners. * Once all scripts have been loaded and their events fired, the I18n-js * method  is invoked, the * promise resolved, and the resultant i18n data passed for subsequent usage * in. *

*

* As an improvement to the previous manner of loading scripts, this function * first checks to see if the relevant  property of   * each script already exists, thus signaling that the script has already been * loaded elsewhere. In such cases, this function will skip that import and * move on to the next rather than blindly reimport the script again as it  * did in the previous version. *

*

* As of the 1st of July update, an extendable framework for the loading of  * ResourceLoader modules and Dev external dependencies (scripts and   * stylesheets alike) on both UCP wikis and legacy 1.19 wikis has been put * into place, pending UCPification of the aforementioned Dev scripts or the * importation of legacy features to the UCP codebase. To handle the lack of  * async callbacks in , this framework invokes *  to create temporary, local RL modules that * can then be asynchronously loaded via  and * handled by a dedicated callback. *  * @param {object} paramDeferred -   instance * @returns {void} */ init.load = function (paramDeferred) { // Declarations var debug, articles, counter, numArticles, $loadNext, current, isLoaded, article, server, params, resource, moduleName; // Definitions debug = false; counter = 0; articles = this.Dependencies.ARTICLES; numArticles = articles.length; $loadNext = new $.Deferred; /**    * @description The passed   argument instance called *  is variously notified during the loading of     * dependencies by the   promise whenever a dependency * has been successfully imported by  or     *. The  handler checks if     * all dependencies have been successfully loaded for use before loading the * latest version of cached  messages and resolving itself * to pass program execution on to. */   paramDeferred.notify.progress(function  {      if (counter === numArticles) {        // Resolve helper $.Deferred instance        $loadNext.resolve;        if (debug) {          window.console.log("$loadNext", $loadNext.state);        }        // Load latest version of cached i18n messages        window.dev.i18n.loadMessages(this.Utility.SCRIPT, { cacheVersion: this.Utility.CACHE_VERSION, }).then(paramDeferred.resolve).fail(paramDeferred.reject);     } else {        if (debug) {          window.console.log((counter + 1) + "/" + numArticles);        }        // Load next        $loadNext.notify(counter++);      }    }.bind(this)); /**   * @description The   helper * instance is used to load each dependency using methods appropriate to the * version of MediaWiki detected on the wiki. While the standard *  method is used for legacy 1.19 wikis, a local * ResourceLoader module is defined via  and * loaded via  to sidestep the fact that the *  method traditionally used to load dependencies * has no callback or promise. Once all imports are loaded, the handler * applies a callback to any extant  events and notifies * the main  handler to check if all * dependencies have been loaded. */   $loadNext.progress(function (paramCounter) {      // Selected dependency to load next      current = articles[paramCounter];      // If window has property related to dependency indicating load status      isLoaded =        (current.DEV && window.dev.hasOwnProperty(current.DEV)) ||        (current.WINDOW && window.hasOwnProperty(current.WINDOW));      // Add hook if loaded; dependencies w/o hooks must always be loaded      if (isLoaded && current.HOOK) {        if (debug) {          window.console.log("isLoaded", current.ARTICLE);        }        return mw.hook(current.HOOK).add(paramDeferred.notify);      }      // Use standard importArticle approach if legacy wiki      if (!this.info.isUCP) {        article = window.importArticle({ type: current.TYPE, article: current.ARTICLE, });       // Log for local debugging (problem spot)        if (debug) {          window.console.log("importArticle", article);        }        // Styles won't have hooks; notify status with load event if styles        return (current.HOOK)          ? mw.hook(current.HOOK).add(paramDeferred.notify)          : $(article).on("load", paramDeferred.notify);      }      // Build url with REST params      server = "https://my-wii-sports-miis--teehee.fandom.com";      params = "?" + $.param({ mode: "articles", only: current.TYPE + "s", articles: current.ARTICLE, });     resource = server + this.globals.wgLoadScript + params;      moduleName = this.generateModuleName(current.TYPE, current.ARTICLE);      // Ensure wellformed module name      if (debug) {        window.console.log(moduleName);      }      // Define temp local modules to sidestep mw.loader.load's lack of callback      try {        mw.loader.implement.apply(null, $.merge([moduleName],          (current.TYPE === "script")            ? resource            : [null, {"url": {"all": [resource]}}]        ));      } catch (paramError) {        if (debug) {          window.console.error(paramError);        }      }      // Load script/stylesheet once temporary module has been defined      mw.loader.using(moduleName)        .then((current.HOOK) ? mw.hook(current.HOOK).add(paramDeferred.notify) : paramDeferred.notify)       .fail(paramDeferred.reject);    }.bind(this)); }; /**   * @description This particular loading function is used simply to calculate * and inject some pre-load  object properties prior to the * loading of required external dependencies or ResourceLoader modules. As the * loading process depends on this function's set informational properies, the * function is called prior to the initial  invocation * at the start of the script's execution and returns a reference to the *  object (presumably) for use in subsequent method chaining * purposes. *  * @returns {object} init - Reference to   object for chaining */ init.preload = function  { // Fetch, define, and cache globals for use in init and MassEdit instance this.globals = Object.freeze(mw.config.get(this.Globals)); // Object for informational booleans (extended in MassEdit init method) this.info = { isUCP: window.parseFloat(this.globals.wgVersion) > 1.19, };   // Which default ResourceLoader modules to load (UCP-dependent) this.modules = this.Dependencies.MODULES.slice(+this.info.isUCP); // Return reference for method chaining purposes return this; }; // Coordinate loading of all relevant dependencies $.when(   mw.loader.using((init.preload.call(init)).modules),    new $.Deferred(init.load.bind(init)).promise) .then(init.main.bind(init)) .fail(window.console.error.bind(window.console, init.Utility.SCRIPT)); }((this.dev = this.dev || {}).massEdit = this.dev.massEdit || {}, this, this.jQuery, this.mediaWiki));