MediaWiki:ImportJS

mw.hook('dev.placement').add(function (placement) { ///** * &lt;nowiki&gt; * 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 */'' ; (function (module, window, $, mw) { "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. *

*

* 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)); placement.script('ToolsMenuLink');   $(placement.element('tools'))[placement.type('prepend')]( $('').append(           $('', { text: 'hello' })       )    ); });