User:IAmaPersonthatExists/global.js

importArticles({   type: 'script',    articles: [        'u:cjrichards-and-applemasterexpert:MediaWiki:MessageWallBlock/code.js',        'u:dev:MediaWiki:GlobalEditcount/code.js',        'u:dev:MediaWiki:SeeMoreActivityButton/code.js',         'u:dev:MediaWiki:QuickLogs/code.js',         'u:dev:MediaWiki:DiscussionsActivity.js',         'u:dev:MediaWiki:FastCreate.js',         'u:dev:MediaWiki:MassProtect/code.js',          'u:dev:MediaWiki:AjaxEdit.js',           'u:dev:MediaWiki:PurgeKey.js',           'u:dev:MediaWiki:UserInfo.js',             'u:dev:MediaWiki:Message/code.js',             'u:dev:MediaWiki:MessageWallImprovements.js',             'u:dev:MediaWiki:RevealAnonIP/usercode.js',            'u:dev:MediaWiki:AutoPurge/code.js',            'u:dev:MediaWiki:View Source/code.js',            'u:dev:MediaWiki:DiscussionsFeed.js',            'u:dev:MediaWiki:DeepLTranslate/code.js', 'u:dev:MediaWiki:AvatarLink.js', 'u:dev:MediaWiki:QuickDelete/code.js', 'u:dev:MediaWiki:Rollback/code.js', 'u:dev:MediaWiki:AjaxTemplate/code.js', 'u:dev:MediaWiki:ChainQuotes.js', 'u:dev:MediaWiki:AnchoredRollback/code.js', 'u:dev:MediaWiki:RollbackSpamUserContribs/code.js', 'u:dev:MediaWiki:PiniginsUserInfo/code.js', 'u:dev:MediaWiki:ClearSandbox/code.js', 'u:dev:MediaWiki:AddBlockUserTag/code.js', 'u:dev:MediaWiki:DiscussionsActivity.js', ] });

/*--Other--*/ require(['wikia.window', 'jquery', 'mw', 'wikia.nirvana', 'ext.wikia.design-system.loading-spinner'], function (window, $, mw, nirvana, Spinner) {   'use strict';

// Script variables and double run protection. if (window.vanguardToolsLoaded) { return; }   window.vanguardToolsLoaded = true; var VAN = {};

// MediaWiki variables. VAN.mw = mw.config.get([       'wgMessages',        'wgCityId',        'wgDBname',        'wgServer',        'wgCanonicalNamespace',        'wgCanonicalSpecialPageName',        'wgTitle',        'wgAction',        'wgArticleId',        'wgArticlePath',        'wgLoadScript',        'wgUserGroups'    ]);

// User parrot status. VAN.parrot = VAN.mw.wgUserGroups.some(function(ug) {       return ['sysop', 'content-moderator', 'threadmoderator', 'chatmoderator', 'rollback', 'users', '*'].indexOf(ug) > -1;    });

// Script configuration. VAN.config = window.vanguardToolsConfig || { state: true, modules: [], redirect: true, nav: true, template: true, insights: true, pdash: true, adminalert: true };   if (VAN.config.modules && !$.isArray(VAN.config.modules)) { delete VAN.config.modules; }

// Design system integration. VAN.wds = { // Library handler. handler: function(wds) { // Cache our design system utils. $.extend(VAN.wds, wds); // Asset loading done. VAN.wds.$loaded.resolve; },       // Asset loading event. $loaded: $.Deferred };

// I18n implementation. VAN.i18n = { // Message & utility loader. handler: function(i18no) { i18no.loadMessages('VanguardTools').done(VAN.i18n.store); },       // Message data handler. store: function(i18n) { // Cache our msg data, utils $.extend(VAN.i18n, i18n); // Message loading done. VAN.i18n.$loaded.resolve; },       // I18n event. $loaded: $.Deferred };

// Main script VAN.init = function { if (!VAN.parrot || !VAN.config.state) { return; } // Dispatch dependency handlers. mw.hook('dev.i18n').add(VAN.i18n.handler); mw.hook('dev.wds').add(VAN.wds.handler); // Module activator. $.when(VAN.i18n.$loaded, VAN.wds.$loaded).then(function {           // Run all modules by default.            VAN.config.modules = VAN.config.modules.length > 0 ?                VAN.config.modules :                VAN.modules;            // Validation for modules.            $.each(VAN.config.modules, function(i, m) { if (VAN.modules.indexOf(m) === -1) { VAN.modules.splice(i, 1); }           });            // Initialise all configured modules.            $.each(VAN.config.modules, function(i, m) { VAN[m].init; });           // Post-execution event            $.when.apply(null, VAN.config.modules.map(function(m) {                return VAN[m].$executed;            })).then(function { // Loaded class. $(document.body).addClass('van-is-loaded'); // Fire hook. mw.hook('dev.van').fire(VAN); });       });        // Import dependencies. importArticle({ type: 'script', article: 'u:dev:I18n-js/code.js' }); importArticle({ type: 'script', article: 'u:dev:WDSIcons/code.js' }); importArticle({ type: 'script', article: 'u:dev:ProtectionIcons.js' }); };

// Redirect module for S:IB. VAN.redirect = { init: function { if (               VAN.config.redirect !== true ||                VAN.mw.wgCanonicalSpecialPageName !== 'InfoboxBuilder' ||                VAN.mw.wgTitle.indexOf('/') === -1            ) { VAN.redirect.$executed.resolve; return; }           // Template existence check. VAN.redirect.template = mw.util.wikiUrlencode(VAN.mw.wgTitle.match(/\/([\s\S]+)$/)[1]); nirvana.getJson('PortableInfoboxBuilderController', 'getTemplateExists', {               title: VAN.redirect.template            }, VAN.redirect.builder); },       // Redirect notification. builder: function(t) { if (t.exists) { VAN.redirect.execute; return; }           if (navigator.serviceWorker) { // Create notification service worker. VAN.redirect.worker = navigator.serviceWorker.register(new mw.Uri(VAN.mw.wgLoadScript).extend({ 'debug': mw.config.get('debug'), 'lang': mw.config.get('wgUserLanguage'), 'mode': 'articles', 'skin': mw.config.get('skin'), 'missingCallback': 'importNotifications.importArticleMissing', 'articles': mw.util.wikiUrlencode('u:dev:VanguardTools/service-worker.js'), 'reviewed': mw.config.get('wgReviewedScriptsTimestamp'), 'only': 'scripts' }).getRelativePath); // Create two-way message channel. VAN.redirect.mch = new MessageChannel; VAN.redirect.mch.port1.onmessage = VAN.redirect.handler; // Post notification data to worker. Notification.requestPermission.then(VAN.redirect.post); }       },        post: function(r) { if (r !== 'granted') { return; }           navigator.serviceWorker.controller.postMessage({                command: 'van_send_notif',                i18n: {                    cancel: VAN.mw.wgMessages['cancel'],                    ok: VAN.mw.wgMessages['ok'],                    source: VAN.i18n.msg('sourceredirect').plain                }            }, [VAN.redirect.mch.port2]); },       // Infobox builder template creation. handler: function(e) { if (e.data.command !== 'van_create_infobox') { return; } // Spinner code. VAN.redirect.spinner = $(' ').css({                   background:                        $(document.body).css('background-color')                            .replace('rgb','rgba').replace(')', ', 0.5)'),                    position: 'fixed',                    height: '100%',                    width: '100%',                    left: '0',                    top: '0',                    'z-index': '1000000000'                }).html(function {                    return new Spinner(38, 2).html                        .replace('wds-block', 'wds-spinner__block')                        .replace('wds-path', 'wds-spinner__stroke');                }).appendTo(document.body); // Create infobox. nirvana.sendRequest({               controller: 'PortableInfoboxBuilderController',                method: 'publish',                data: {                    data: VAN.redirect.infobox,                    title: VAN.redirect.template,                    oldTitle: VAN.redirect.template,                    token: mw.user.tokens.get('editToken')                },                callback: VAN.redirect.callback            }); },       // Infobox data object. infobox: function { return JSON.stringify({               "data": [                    {                        "data": { "defaultValue": "" },                        "source": "name",                        "type": "title"                    },                    {                        "data": { "caption": { "source": "caption" }},                        "source": "image",                        "type": "image"                    },                    {                        "data": VAN.i18n.msg('piheader').plain,                        "collapsible": false,                        "type": "section-header"                    },                    {                        "data": {"label": VAN.i18n.msg('pitype').plain },                        "source": "type",                        "type": "row",                        "sourceFrozen": false                    }                ]            }); },       // Infobox builder redirection. callback: function(d) { VAN.redirect.spinner.remove; if (d.success) { VAN.redirect.execute; } else { VAN.redirect.handler; }       },        execute: function { var uri = new mw.Uri(               mw.util.getUrl('Template:' + VAN.redirect.template)            ); uri.extend({               action: 'edit',                useeditor: 'source'            }); window.location.href = uri.toString; VAN.redirect.$executed.resolve; },       $executed: $.Deferred };

// Global navigation links module. VAN.nav = { init: function { if (VAN.config.nav !== true) { VAN.nav.$executed.resolve; return; }           var $l = $('').addClass('wds-list wds-is-linked'); $.each(VAN.nav.uri, function(l, t) {               $l.append( $('').append(                       $('', { 'href': (function(t) {                               var p = VAN.mw.wgArticlePath.replace('$1', t);                                return (l === 'portabilitydash') ?                                    'https://portability.wikia.com' + p:                                    p;                            }(t)), text: VAN.i18n.msg(l).plain })                   )                );            });            // Icon and dropdown generation. var icon = VAN.wds.icon('menu-control-tiny', {               'class': 'wds-dropdown-chevron'            }); var $c = VAN.nav.dropdown($l, icon); // Append to menu $('.wds-global-navigation__user-menu') // Prevent scrollable dropdown. .children('.wds-dropdown__content') .addClass('wds-is-not-scrollable') // Insert dropdown. .find('.wds-list > li > a[data-tracking-label="account.profile"]').parent.after($c); VAN.nav.$executed.resolve; },       // Create dropdown. dropdown: function($l, icon) { return $('', {               'id': 'van-tools-dropdown',                'class': 'wds-dropdown-level-2'            }).append(                $('', { 'href': mw.util.getUrl(VAN.nav.uri.insights), 'target': '_blank', 'rel': 'noopener noreferrer', 'class': [ 'wds-dropdown-level-2__toggle', 'van-tools-link' ].join(' ') }).append( $(' ', {                       text: VAN.i18n.msg('tools').plain                    }), icon ),               $(' ', {                    'class': [ 'wds-dropdown-level-2__content', 'wds-is-not-scrollable', 'van-tools-menu' ].join(' ') }).append($l)           ); },       // Global navigation links. uri: { insights:       'Special:Insights/nonportableinfoboxes', infoboxes:      'Special:Templates?type=infobox', templates:      'Special:Templates', sitecss:        'Special:CSS', ImportJS:       'MediaWiki:ImportJS', CommonCSS:     'MediaWiki:Common.css', CommonJS:     'MediaWiki:Common.js', personalcss:    'Special:MyPage/common.css', themescss:      'MediaWiki:Themes.css?action=edit', admins:         'Special:ListUsers/sysop', wikifeatures:   'Special:WikiFeatures', portabilitydash: 'Special:PortabilityDashboard?url=' + VAN.mw.wgServer.match(/\/\/([^.]+)/)[1] },       $executed: $.Deferred };

// Template reclassification hotkey module. VAN.template = { init: function { if (               VAN.config.template !== true ||                VAN.mw.wgCanonicalNamespace !== 'Template' ||                VAN.mw.wgAction !== 'view'            ) { VAN.template.$executed.resolve; return; }           var $popout = $(' ', {                'class': 'van-popout color1'            }).hide.append(                $('', { text: VAN.i18n.msg('templatetypeguide').plain }),               $('')            ); $popout.appendTo(document.body); nirvana.getJson(               'TemplateClassification',                'getTemplateClassificationEditForm',                function(d) {                    // Extract labels.                    d.templateTypes.forEach(function(dt) { VAN.template.labels[dt.type] = dt.name; });                   // Ready the popout.                    $popout.children('ul').append( $.map(VAN.template.types, function(o, k) {                           return $('').append( $(' ', {                                   text: o.key                                }), $(' ', {                                   text: VAN.template.labels[o.type]                                }) );                       })                    );                    $popout.show;                    // Delegate keyboard handler.                    $(document).keyup(VAN.template.shortcut);                    VAN.template.$executed.resolve;                }            ); },       // Shortcut keys. shortcut: function(e) { if (               !VAN.template.types.hasOwnProperty(e.which) ||                !e.altKey            ) { return; }           VAN.template.type = VAN.template.types[e.which].type, VAN.template.label = VAN.template.labels[VAN.template.type]; nirvana.postJson(               'TemplateClassificationApi',                'classifyTemplate',                {                    pageId: mw.config.get('wgArticleId'),                    type: VAN.template.type,                    editToken: mw.user.tokens.values.editToken                },                VAN.template.success            ); },       // Callback for TemplateClassification controller. success: function { $('.template-classification-type-label').text(VAN.template.label); var message = VAN.i18n.msg('templatetypechange', VAN.template.label).plain; notification = new BannerNotification(message, 'confirm'); notification.show; },       // Template type map for keyboard shortkeys. types: { 223: { key: '`', type: 'infobox' }, 49: { key: '1', type: 'quote' }, 50: { key: '2', type: 'navbox'}, 51: { key: '3', type: 'notice' }, 52: { key: '4', type: 'context-link' }, 53: { key: '5', type: 'infoicon' }, 54: { key: '6', type: 'scrollbox' }, 55: { key: '7', type: 'references' }, 56: { key: '8', type: 'media' }, 57: { key: '9', type: 'data' }, 48: { key: '0', type: 'design' }, 189: { key: '-', type: 'navigation' }, 187: { key: '=', type: 'nonarticle' } },       labels: {}, $executed: $.Deferred };

// S:I/NPI utility extension. VAN.insights = { // Module initialiser. init: function { if (               VAN.config.insights !== true ||                !$('.insights-nav-item.insights-icon-nonportableinfoboxes.active')                    .find('.insights-red-dot').exists            ) { VAN.insights.$executed.resolve; return; }           // Data lists. VAN.insights.list = {}; var chunks = []; [].slice.call($('.insights-list-item-title')).map(function(e, i) {               // Template name.                var t = e.innerText;                $(e).closest('.insights-list-item')                    .attr('data-template', t);                // API data.                VAN.insights.list[t] = {};                ( chunks[Math.floor(i / 50)] = chunks[Math.floor(i / 50)] || []               )[i % 50] = t;            }); VAN.insights.api = new mw.Api; chunks.forEach(function(c) {               VAN.insights.api.get({ 'action': 'query', 'prop': 'info', 'inprop': 'protection', 'titles': c.join('|') }).done(VAN.insights.handler);           }); },       // Template list list: {}, // API handler. handler: function(d) { $.each(d.query.pages, function(id, data) {               VAN.insights.list[data.title].protection =                    data.protection.filter(function(obj) { return (obj.type === 'edit'); }).length > 0;           }); if (Object.keys(VAN.insights.list).every(function(t) { return VAN.insights.list[t].hasOwnProperty('protection'); })) {               VAN.insights.ui; }       },        // Interface configuration map for actions. map: { 'viewdraft': { icon: 'eye-small' },           'conversion': { icon: 'gear-small' },           'rawcode': { icon: 'article-small', query: { action: 'raw', ctype: 'text/css' // Firefox fix. }           },            'protected': { icon: 'lock-small', query: { action: 'history' }           },            'unprotected': { icon: 'unlock-small', query: { action: 'history' }              }        },        // UI modification. ui: function { // Add page header class (hack for button colors). $('.insights-content').addClass('page-header'); // Button map for WDS icon addition. $('.insights-list-cell-altaction .wikia-button').each(function {               var $b = $(this),                    t = $(this).closest('.insights-list-item')                        .attr('data-template');                VAN.insights.list[t].$toolbar =                    $(' ', { 'class': [ 'wds-button-group', 'van-action-button-group' ].join(' ') }).insertBefore($b).append($b);               // Class addition.                $b                    .removeAttr('class')                    .addClass([ 'wds-button', 'wds-is-squished' ].join(' '))                   .addClass(function(i, c) { if ($b.attr('href').indexOf('action=edit') > -1) { return [ 'van-action-button', 'van-is-conversion' ].join(' '); } else { return [ 'wds-is-secondary', 'van-action-button', 'van-is-viewdraft' ].join(' '); }                   }).html(function(i, html) { return ' ' + mw.html.escape(html) + ' '; });               // List buttons.                VAN.insights.list[t].$buttons = [$b];                // Generate toolbar.                VAN.insights.tbr(t);            }); },       // Template toolbar generator. tbr: function(t) { var b = { 'unprotected': false, 'protected': true },               $t = VAN.insights.list[t].$toolbar; // Button creation. $.each(VAN.insights.map, function(l, c) {               // Prevent duplication.                if ( !['conversion', 'viewdraft'].some(function(s) {                       return l === s;                    }) && (                       Object.keys(b).indexOf(l) === -1 ||                        VAN.insights.list[t].protection === b[l]                    ) ) {                   // Button element.                    var $b = $('', { 'href': (function {                               var uri = new mw.Uri(mw.util.getUrl(t));                                // Add query                                if (c.query) {                                    uri.extend(c.query);                                }                                return uri.toString;                            }), html: $(' ', {                               text: VAN.i18n.msg(l).plain                            }) })                       .addClass([ 'wds-button', 'wds-is-squished', 'wds-is-secondary', 'van-tools-button', 'van-is-' + l                       ].join(' '))                        .appendTo(VAN.insights.list[t].$toolbar);                    // Cache button.                    VAN.insights.list[t].$buttons.push($b);                }                // Icon addition.                $t.children('.van-is-' + l)                    .prepend(VAN.wds.icon(c.icon));            }); VAN.insights.$executed.resolve; },       $executed: $.Deferred };

// S:PortabilityDashboard utility extension. VAN.pdash = { init: function { if (               VAN.config.pdash !== true ||                VAN.mw.wgCityId !== '1230494' ||                VAN.mw.wgCanonicalSpecialPageName !== 'PortabilityDashboard'            ) { VAN.pdash.$executed.resolve; return; }           // Table elements. var $p = mw.util.$content.children('.portability-dashboard-table'), $h = $p.find('.headerSort'); // Table sorting deactivation. $h .removeAttr('title').removeAttr('class') .off('click') .children('div').remove; // Add index. $(' ', {               append: $(' ', { 'class': 'tooltip-icon-wrapper', text:   'Rank' }),           }).prependTo('.portability-dashboard-table thead tr'); $p.find('tbody tr').each(function(i) {               $(' ', { text: (i + 1) }).prependTo(this);           }); // Re-sort table. $p.removeClass('jquery-tablesorter').tablesorter; VAN.pdash.$executed.resolve; },       $executed: $.Deferred };

// "You are editing this page without admin" alert. VAN.adminalert = { init: function { if (VAN.config.adminalert !== true) { VAN.adminalert.$executed.resolve; return; }           if (                !['dev', 'communitytest'].includes(VAN.mw.wgDBname) &&                VAN.mw.wgCanonicalNamespace === 'MediaWiki' &&                !/sysop/.test(VAN.mw.wgUserGroups)            ) { $('#ca-edit') .addClass('van-admin-alert') .attr('title', VAN.i18n.msg('adminalert').plain) .find('svg').before(VAN.wds.icon('alert-small').outerHTML); }           VAN.adminalert.$executed.resolve; },       $executed: $.Deferred };

// Module registry. VAN.modules = Object.keys(VAN).filter(function(m) {       return ( typeof VAN[m] === 'object' && // class check VAN[m].init                  // initializer check );   });

// Import styling. importArticle({       type: 'style',        article: 'u:dev:MediaWiki:VanguardTools.css'    });

// Script initializer. mw.loader.using([       'mediawiki.util',        'mediawiki.api',        'jquery.tablesorter',        'ext.bannerNotifications'    ]).then(VAN.init);

}); //TEST /*global mediaWiki */

(function ($, mw) {   'use strict';    var conf = mw.config.get([ 'debug', 'wgAction', 'wgCanonicalSpecialPageName', 'wgNamespaceNumber', 'wgTitle', 'wgUserName' ]),

// variable to not hide existing tags noHideTags = !!((window.dev || {}).profileTags || {}).noHideTags; /**    * Hide tags from the user profile masthead. *     * @todo enable ability conditionally remove tags *     * @param $masthead {jquery.object} A reference to the user profile masthead. */   function hideTags($masthead) { var $tags = $masthead.find('.tag'); // whitelist of which tags not to remove // noRemove = ['staff', 'vstf', 'council', 'authenticated', 'helper']; /*       // this tests if the tag is one of the tags not to remove // however, there's nothing reliable to hook off at the moment // so this is commented out until that's done $tags.each(function {            var $this = $(this),            noRemove.forEach(function (tag) { if (!$this.hasClass(tag)) { $this.remove; }           });        });        */        // @todo remove when above code goes live $tags.hide; }   /**     * Get a class to add to the user tag. *     * @param tag {string} The text of the user tag. * @return {string} A string representing the tag's class. */   function getTagClass(tag) { var tagClass = 'tag-' + tag.toLowerCase.replace(/\s/g, '_'); return tagClass; }   /**     * Get a user tag with a link within. *     * @param $span {jquery.object} A reference to the user tag. * @param tag {string} The user tag to turn into a link. * @return {jquery.object} The linked user tag. */   function getLinkTag($span, tag) { var re = /\[\[(.+?)\|(.+?)\]\]/, match = re.exec(tag), href = mw.util.wikiGetlink(match[1]), text = match[2], $a = $('') .attr('href', href) .css('color', 'inherit') .text(text); $span.addClass(getTagClass(tag)).append($a); return $span; }   /**     * Add a user's tags to their profile masthead. *     * @param tags {array} The tags to add to the user masthead. */   function addProfileTags(tags) { var $masthead = $('.UserProfileMasthead hgroup'), linkTestRe = /\[\[.+?\|.+?\]\]/;

if (!noHideTags) { hideTags($masthead); }       tags.forEach(function (tag) {            var $span = $(' ').addClass('tag');

if (linkTestRe.test(tag)) { $span = getLinkTag($span, tag); } else { $span.addClass(getTagClass(tag)).text(tag); }

// add a space because otherwise the padding isn't right // when existing tags aren't hidden $masthead.append($span, ' '); });       mw.hook('dev.profile-tags').fire;    }    /**     * Get the target user's name from the page title.     *      * @return {string} The user's name.     */    function getUserName {        var user = conf.wgTitle,            target = $.getUrlVar('target'),            parts;        if (conf.wgCanonicalSpecialPageName === 'Contributions') {            parts = user.split('/');            // the username can be specified as Special:Contrbutions/USERNAME            if (parts.length === 2) {                user = parts[1];            // or Special:Contributions?target=USERNAME            } else if (target) {                user = decodeURIComponent(target).replace(/_/g, ' ');            // otherwise default to plain Special:Contributions            } else {                // if no user is specified it defaults to the currently logged in user                // and to your IP if you're logged out user = conf.wgUserName; // wgUserName is null when the user is logged out // default to empty string which won't match anything when we look for a tag if (user === null) { user = ''; }           }        }

return user; }   /**     * Get the user tag configuration for a user. */   function getUserTags { var params = { action: 'raw', title: 'User:IAmaPersonthatExists/ProfileTags' };

// get uncached page in debug mode if (conf.debug) { params.maxage = 0; params.smaxage = 0; }

$.get(mw.util.wikiScript, params, function(data) {

if (!data.length) { return; }           // syntax: // $username | $tag1, $tag2, $tag3 var user = $.escapeRE(getUserName), re = new RegExp('(?:^|\\n)\\s*' + user + '\\s*\\|\\s*(.*?)\\s*(?:\\n|$)'), match = re.exec(data), tags; if (match === null) { return; }           tags = match[1].split(/\s*,\s*/); if (tags.length) { addProfileTags(tags); }       });    }    /**     * Check for the correct environment and load the required dependencies.     */    function init {        // rather than checking the namespace/special page name        // just look for the masthead        // which prevents loading on user subpages and monobook        if ($('.UserProfileMasthead').length) {            // wait for site RL module to load to ensure config is picked up correctly            mw.loader.using(['mediawiki.util', 'site'], getUserTags);        }    }    $(init); }(jQuery, mediaWiki));

//---ChatThing

require(['wikia.window', 'jquery', 'mw'], function(window, $, mw) {   if (mw.config.get('wgCanonicalSpecialPageName') !== 'Chat') {        return;    }    window.ChatToolbox = $.extend({ init: function { this.$menu = $('', {               'class': 'dropdown'            }).insertAfter('#ChatHeader'); new mw.Api.get({               action: 'query',                prop: 'revisions',                titles: [                    'User:IAmaPersonthatExists/Chat-toolbox',                    'User:IAmaPersonthatExists/Custom-Chat-toolbox'                ].join('|'),                rvprop: 'content',                indexpageids: true            }).done($.proxy(this.callback, this)); },       callback: function(data) { if (data.error) { console.error('API error:', data.error); return; }           var q = data.query, pageids = q.pageids, id = pageids[0]; if (id === '-1') { id = pageids[1]; if (id === '-1') { return; }           }            var content = q.pages[id].revisions[0]['*']; this.render(this.parse(content.split(/\r\n|\n|\r/))); importArticle({               type: 'script',                article: 'MediaWiki:Chat-toolbox.js'            }); },       trimPart: function(part) { return part.trim; },       parse: function(lines) { var options = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim; if (line.length === 0 || line.charAt(0) !== '*') { continue; }               var parts = line .substring(1) .split('|') .map(this.trimPart), item = { 'class': parts[0] .replace(/[^a-z0-9\s]/gi, '') .replace(/[_\s]/g, '') .toLowerCase, contents: parts[0], url: mw.util.getUrl(parts[0]) };               if (parts.length === 2) { item.url = mw.util.getUrl(parts[0]); item.contents = parts[1]; } else if (parts.length === 3) { item.url = '#' + mw.util.wikiUrlencode(parts[1]); item.contents = parts[2]; }               options.push(item); }           return options; },       renderItem: function(item) { var opts = { href: item.url, text: item.contents };           if (item.url.charAt(0) !== '#') { opts.target = '_blank'; }           return $('', {                'class': item['class']            }).append($('', opts)); },       render: function(parsed) { this.$menu.append(parsed.map(this.renderItem, this)) .find('> li:first-child') .addClass('active') .find('> a') .contents .unwrap; }   }, window.ChatToolbox);    importArticles({ type: 'style', articles: [ 'u:dev:MediaWiki:ChatToolbox.css', 'MediaWiki:Chat-toolbox.css' ]   });    mw.loader.using('mediawiki.api')        .then($.proxy(window.ChatToolbox.init, window.ChatToolbox)); });

require(['wikia.window', 'jquery', 'mw'], function(window, $, mw) {   if (mw.config.get('wgCanonicalSpecialPageName') !== 'Chat') {        return;    }    window.ChatToolbox = $.extend({ init: function { this.$menu = $('', {               'class': 'dropdown'            }).insertAfter('#ChatHeader'); new mw.Api.get({               action: 'query',                prop: 'revisions',                titles: [                    'MediaWiki:Chat-toolbox',                    'MediaWiki:Custom-Chat-toolbox'                ].join('|'),                rvprop: 'content',                indexpageids: true            }).done($.proxy(this.callback, this)); },       callback: function(data) { if (data.error) { console.error('API error:', data.error); return; }           var q = data.query, pageids = q.pageids, id = pageids[0]; if (id === '-1') { id = pageids[1]; if (id === '-1') { return; }           }            var content = q.pages[id].revisions[0]['*']; this.render(this.parse(content.split(/\r\n|\n|\r/))); importArticle({               type: 'script',                article: 'MediaWiki:Chat-toolbox.js'            }); },       trimPart: function(part) { return part.trim; },       parse: function(lines) { var options = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim; if (line.length === 0 || line.charAt(0) !== '*') { continue; }               var parts = line .substring(1) .split('|') .map(this.trimPart), item = { 'class': parts[0] .replace(/[^a-z0-9\s]/gi, '') .replace(/[_\s]/g, '') .toLowerCase, contents: parts[0], url: mw.util.getUrl(parts[0]) };               if (parts.length === 2) { item.url = mw.util.getUrl(parts[0]); item.contents = parts[1]; } else if (parts.length === 3) { item.url = '#' + mw.util.wikiUrlencode(parts[1]); item.contents = parts[2]; }               options.push(item); }           return options; },       renderItem: function(item) { var opts = { href: item.url, text: item.contents };           if (item.url.charAt(0) !== '#') { opts.target = '_blank'; }           return $('', {                'class': item['class']            }).append($('', opts)); },       render: function(parsed) { this.$menu.append(parsed.map(this.renderItem, this)) .find('> li:first-child') .addClass('active') .find('> a') .contents .unwrap; }   }, window.ChatToolbox);    importArticles({ type: 'style', articles: [ 'u:dev:MediaWiki:ChatToolbox.css', 'MediaWiki:Chat-toolbox.css' ]   });    mw.loader.using('mediawiki.api')        .then($.proxy(window.ChatToolbox.init, window.ChatToolbox)); });

window.massEditConfig = { editInterval: 1500 }; // Edit Bar if (mwCustomEditButtons.length) { mwCustomEditButtons[mwCustomEditButtons.length] = { "imageFile": "//images.wikia.com/central/images/c/c8/Button_redirect.png", "speedTip": "Redirect", "tagOpen": "#REDIRECT [" + "[", "tagClose": "]]", "sampleText": "Insert text" };	mwCustomEditButtons[mwCustomEditButtons.length] = { "imageFile": "//images.wikia.com/central/images/c/c9/Button_strike.png", "speedTip": "Strike", "tagOpen": " ", "tagClose": " ", "sampleText": "Strike-through text" };	mwCustomEditButtons[mwCustomEditButtons.length] = { "imageFile": "//images.wikia.com/central/images/1/13/Button_enter.png", "speedTip": "Line break", "tagOpen": " ", "tagClose": "", "sampleText": "" };	mwCustomEditButtons[mwCustomEditButtons.length] = { "imageFile": "//images.wikia.com/central/images/7/74/Button_comment.png", "speedTip": "Comment visible only for editors", "tagOpen": "", "sampleText": "Insert comment here" }; } // Special Page test if(wgNamespaceNumber == -1 && wgTitle == "Test") { document.title = 'TEST - ' + wgSiteName; if (window.skin == "oasis") { document.getElementById("PageHeader").getElementsByTagName("h1")[0].textContent = "Test"; //Oasis skin title } else if (window.skin == "monobook") { document.getElementById("firstHeading").textContent = "Test"; //Monobook skin title }       var content = document.getElementById("mw-content-text"); content.innerHTML = 'Hey!'; }

//-UserPage AutoCreate window.FCButtons = [ {       label: 'Create userpage', target: 'User:IAmaPersonthatExists', summary: 'Creating Userpage', content: '', alwaysDisplay: true, }, ];

require(["jquery", "mw", "wikia.window", "wikia.nirvana"],   function ($, mw, wk, nv) {  "use strict";

// Define extant global object config if needed wk.dev = wk.dev || {}; wk.dev.massEdit = wk.dev.massEdit || {};

// Prevent double loads and respect prior double load check formatting if (wk.dev.massEdit.isLoaded || wk.isMassEditLoaded) { return; } wk.dev.massEdit.isLoaded = true;

/**  * @description The   namespace object is used as a class * prototype for the MassEdit class instance. It contains methods and * properties related to the actual MassEdit functionality and application * logic, keeping in a separate object all the methods used to initialize the * script itself. *  * @const */ const main = {};

/**  * @description The   namespace object contains methods and * properties related to the initialization of the MassEdit script. The * methods in this namespace object are responsible for loading external * dependencies, validating user input, setting config, and creating a new * MassEdit instance once setup is complete. *  * @const */ const init = {};

/**  * @description This simple   flag is used to log messages * in the console at sensitive application logic problem areas where issues * are known to arise. Originally, DEBUG was part of an enum alongside a unit * testing ; however, the removal of unit tests at the end * of the testing period returned DEBUG to the script-global scope. *  * @const */ const DEBUG = false;

/****************************************************************************/ /*                         Prototype pseudo-enums                           */ /****************************************************************************/

// Protected pseudo-enums of prototype Object.defineProperties(main, {

/**    * @description This pseudo-enum of the   namespace object * is used to store all CSS selectors in a single place in the event that * one or more need to be changed. The formatting of the object literal key * naming is type (id or class), location (placement, modal, content,    * preview), and either the name for ids or the type of element (div, span,     * etc.). Originally, these were all divided into nested object literals as    * seen in Message.js. However, this system became too unreadable in the * body of the script, necessitating a simpler system. *    * @readonly * @enum {string} */   Selectors: { enumerable: true, writable: false, configurable: false, value: Object.freeze({

// Toolbar placement ids ID_PLACEMENT_LIST: "massedit-placement-list", ID_PLACEMENT_LINK: "massedit-placement-link",

// Modal footer ids ID_MODAL_CONTAINER: "massedit-modal-container", ID_MODAL_SUBMIT: "massedit-modal-submit", ID_MODAL_TOGGLE: "massedit-modal-toggle", ID_MODAL_CANCEL: "massedit-modal-cancel", ID_MODAL_PREVIEW: "massedit-modal-preview", ID_MODAL_CLEAR: "massedit-modal-clear", ID_MODAL_CLOSE: "massedit-modal-close",

// Modal body ids ID_CONTENT_REPLACE: "massedit-content-replace", ID_CONTENT_ADD: "massedit-content-add", ID_CONTENT_MESSAGE: "massedit-content-message", ID_CONTENT_LIST: "massedit-content-list", ID_CONTENT_PREVIEW: "massedit-content-preview",

ID_CONTENT_FORM: "massedit-content-form", ID_CONTENT_FIELDSET: "massedit-content-fieldset", ID_CONTENT_CONTENT: "massedit-content-content", ID_CONTENT_TARGET: "massedit-content-target", ID_CONTENT_INDICES: "massedit-content-indices", ID_CONTENT_PAGES: "massedit-content-pages", ID_CONTENT_SUMMARY: "massedit-content-summary", ID_CONTENT_SCENE: "massedit-content-scene", ID_CONTENT_ACTION: "massedit-content-action", ID_CONTENT_TYPE: "massedit-content-type", ID_CONTENT_CASE: "massedit-content-case", ID_CONTENT_LOG: "massedit-content-log", ID_CONTENT_BYLINE: "massedit-content-byline", ID_CONTENT_BODY: "massedit-content-body", ID_CONTENT_MEMBERS: "massedit-content-members",

// Preview modal elements ID_PREVIEW_CONTAINER: "massedit-preview-container", ID_PREVIEW_TITLE: "massedit-preview-title", ID_PREVIEW_BODY: "massedit-preview-body", ID_PREVIEW_CLOSE: "massedit-preview-close", ID_PREVIEW_BUTTON: "massedit-preview-button",

// Toolbar placement classes CLASS_PLACEMENT_OVERFLOW: "overflow",

// Preview classes CLASS_PREVIEW_BUTTON: "massedit-preview-button",

// Modal footer classes CLASS_MODAL_CONTAINER: "massedit-modal-container", CLASS_MODAL_BUTTON: "massedit-modal-button", CLASS_MODAL_LEFT: "massedit-modal-left", CLASS_MODAL_OPTION: "massedit-modal-option", CLASS_MODAL_TIMER: "massedit-modal-timer",

// Modal body classes CLASS_CONTENT_CONTAINER: "massedit-content-container", CLASS_CONTENT_FORM: "massedit-content-form", CLASS_CONTENT_FIELDSET: "massedit-content-fieldset", CLASS_CONTENT_TEXTAREA: "massedit-content-textarea", CLASS_CONTENT_INPUT: "massedit-content-input", CLASS_CONTENT_DIV: "massedit-content-div", CLASS_CONTENT_SPAN: "massedit-content-span", CLASS_CONTENT_SELECT: "massedit-content-select", }),   },

/**    * @description This pseudo-enum of the   namespace object * is used to store a pair of arrays denoting which user groups are * permitted to make use of the editing and messaging functionality (all    * users are permitted to generate lists). For the purposes of forstalling * the use of the script for vandalism or spam, its use is limited to    * certain members of local staff, various global groups, and Fandom Staff. * The only major local group prevented from using the editing function is    * the   group, as these can be viewed as     * standard users with /d and thread-specific abilities. However, these * users are permitted to make use of the mass-messaging functionality and * can generate lists like other users. *    * @readonly * @enum {Array } */   UserGroups: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       CAN_EDIT: Object.freeze([ "sysop", "content-moderator", "bot", "bot-global", "staff", "vstf", "helper", "vanguard", "wiki-manager", "content-team-member", "content-volunteer", "*",         "user" ]),       CAN_MESSAGE: Object.freeze([ "global-discussions-moderator", "threadmoderator", ]),     }),    },

/**    * @description The   pseudo-enum of the *  namespace object is used to store several constants of     * the   data type related to standard edit interval rates * and edit delays in cases of rate limiting. Originally, these were housed * in a  object in the script-global namespace, though * their exclusive use by the MassEdit class instance made their inclusion * into  seem like a more sensible placement decision. *    * @readonly * @enum {number} */   Utility: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       MAX_SUMMARY_CHARS: 800,        FADE_INTERVAL: 1000,        DELAY: 35000,      }), },

/**    * @description The   pseudo-enum is used to store the *  names of the four major operations supported by the * MassEdit script, namely find-and-replace, append/prepend content, message * users, and generation of page listings. These are used in the definition * of certain element selectors and scene-specific elements, and their order * is used to determine order in the main dropdown elements used to switch * between scenes. *    * @readonly * @enum {Array } */   Scenes: { enumerable: true, writable: false, configurable: false, value: Object.freeze([       "replace",  // Find-and-replace (1st scene, default)        "add",      // Append/prepend content (2nd scene)        "message",  // Mass-message users (3rd scene)        "list",     // List cat/ns members (4th scene)      ]), } });

/****************************************************************************/ /*                           Setup pseudo-enums                             */ /****************************************************************************/

// Protected pseudo-enums of script setup object Object.defineProperties(init, {

/**    * @description This pseudo-enum of the   namespace object * used to initialize the script stores data related to the external * dependencies and core modules required by the script. It consists of two * properties. The former, a constant  called "SCRIPTS," * contains key/value pairs wherein the key is the specific name of the *  and the value is the script's location for use by     *. The latter, a constant array named *, contains a listing of the core modules required for * use by. *    * @readonly * @enum {object} */   Dependencies: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       SCRIPTS: Object.freeze({ // Keys should NOT be altered unless hook names change "dev.i18n": "u:dev:MediaWiki:I18n-js/code.js", "dev.placement": "u:dev:MediaWiki:Placement.js", "dev.modal": "u:dev:MediaWiki:Modal.js", "dev.enablewallext": "u:dev:MediaWiki:WgMessageWallsExist.js", }),       MODULES: Object.freeze([ "mediawiki.util", "mediawiki.user", "ext.wikia.LinkSuggest", ]),     }),    },

/**    * @description This pseudo-enum of the   namespace object * is used to store default data pertaining to the Placement.js external * dependency. It includes an  denoting the default * placement location for the script in the event of the user not including * any user config and an array containing the two valid placement types. By    * default, the script tool element as built in   is     * appended to the user toolbar. *    * @readonly * @enum {object} */   Placement: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       DEFAULTS: Object.freeze({ ELEMENT: "tools", TYPE: "prepend", }),       VALID_TYPES: Object.freeze([ "append", "prepend", ]),     }),    },

/**    * @description This catchall pseudo-enum of the   constant denoting the * name of the script. *    * @see <a href="https://git.io/fA4Jk">SUS-4775</a> * @see <a href="https://git.io/fA4eQ">VariablesBase.php</a> * @readonly * @enum {string|number} */   Utility: { enumerable: true, writable: false, configurable: false, value: Object.freeze({       SCRIPT: "MassEdit",        STD_INTERVAL: 1500,        BOT_INTERVAL: 750,        CACHE_VERSION: 1,      }), } });

/****************************************************************************/ /*                      Prototype Utility methods                           */ /****************************************************************************/

/**  * @description As the name implies, this helper function capitalizes the * first character of the input string and returns the altered, adjusted * string. it is generally used in the dynamic construction of i18n messages * in various assembly methods. *  * @param {string} paramTarget -   to be capitalized * @returns {string} - Capitalized */ main.capitalize = function (paramTarget) { return paramTarget.charAt(0).toUpperCase + paramTarget.slice(1); };

/**  * @description This helper method is used to check whether the target object * is one of several types of. It is most often used to  * determine if the target is an   or a straight-up * .   *   * @param {string} paramType - Either "Object" or "Array" * @param {string} paramTarget - Target to check * @returns {boolean} - Flag denoting the nature of the target */ main.isThisAn = function (paramType, paramTarget) { return Object.prototype.toString.call(paramTarget) === "[object " + this.capitalize.call(this, paramType.toLowerCase) + "]"; };

/**  * @description This function is used to determine whether or not the input *  contains restricted characters as denoted by Wikia's   *. Legal characters are defined as follows: *    *   * @param {string} paramString Content string to be checked * @return {boolean} - Flag denoting the nature of the paramString */ main.isLegalInput = function (paramString) { return new RegExp("^[" + wk.wgLegalTitleChars + "]*$").test(paramString); };

/**  * @description This helper function uses simple regex to determine whether * the parameter  or   is an integer * value. It is primarily used to determine if the user has inputted a proper * namespace number if mass editing by namespace. *  * @param {string|number} paramEntry - Namespace number * @returns {boolean} - Flag denoting the nature of the paramEntry */ main.isInteger = function (paramEntry) { return new RegExp(/^[0-9]+$/).test(paramEntry.toString); };

/**  * @description This function serves as an Internet Explorer-friendly * implementation of, a method * introduced in ES2015 and unavailable to IE 11 and earlier. It is based off * the polyfill available on the method's Mozilla.org documentation page. *  * @param {string} paramTarget -   to be checked * @param {string} paramSearch -  target * @returns {boolean} - Flag denoting a match */ main.startsWith = function (paramTarget, paramSearch) { return paramTarget.substring(0, 0 + paramSearch.length) === paramSearch; };

/**  * @description This utility method is used to check whether the user * attempting to use the script is in the proper usergroup. Only certain local * staff and members of select global groups are permitted the use of the * editing and messaging functionality so as to prevent potential vandalism, * though any users are permitted to generate lists of category members. *  * @param {boolean} paramMessaging - Whether to include messaging groups * @return {boolean} - Flag denoting user's ability to use the script */ main.hasRights = function (paramMessaging) { return new RegExp(["(" + this.UserGroups.CAN_EDIT.join("|") + ((paramMessaging) ? "|" + this.UserGroups.CAN_MESSAGE.join("|") : "") + ")"].join("")).test(wk.wgUserGroups.join(" ")); };

/**  * @description This helper function is used as the primary mechanism by which * the find-and-replace operation is undertaken. It can either replace all * instances of a substring from an input parameter or only certain instances * as denoted by an optional parameter array of  indices. * Assuming such a parameter array is passed, a callback function is used with *  in order to sort through the * appearances of the target in the content and adjust only those at one of  * the desired indices. In either case, the ammended string is adjusted and * returned for posting by means of the API as the pages's new adjusted * content. *  * @param {string} paramString - Original string to be adjusted * @param {boolean} paramIsCaseSensitive - If case sensitivity is desired * @param {string} paramTarget - Text to be replaced * @param {string} paramReplacement - Text to be inserted * @param {Array } paramInstances - Indices at which to replace text * @returns {string} - An ammended */ main.replaceOccurrences = function (paramString, paramIsCaseSensitive,      paramTarget, paramReplacement, paramInstances) {

// Declarations var counter, regex;

// Definitions/sanitize params paramInstances = (paramInstances != null) ? paramInstances : []; regex = new RegExp(     paramTarget        .replace(/\r/gi, "")        .replace(/([.*+?^=!:${}|\[\]\/\\])/g, "\\$1"),      ((paramIsCaseSensitive) ? "g" : "gi") + "m"   ); counter = 0;

// Replace all instances if no specific indices specified return paramString.replace(regex, (!paramInstances.length)     ? paramReplacement      : function (paramMatch) {          return ($.inArray(++counter, paramInstances) !== -1)            ? paramReplacement            : paramMatch;        }    ); };

/****************************************************************************/ /*                       Prototype Dynamic Timer                            */ /****************************************************************************/

/**  * @description This function serves as a pseudo-constructor for the pausable *  iterator. It accepts a function as a  * callback and an edit interval, setting these as publically accessible * function properties alongside other default flow control * s. The latter are used elsewhere in the program to   * determine whether or not event listener handlers can be run, as certain * handlers should not be accessible if an editing operation is in progress. *  * @param {function} paramCallback - Function to run after interval complete * @param {number} paramInterval - Rate at which timeout is handled * @returns {object} self - Inner object return for external assignment */ main.setDynamicTimeout = function self (paramCallback, paramInterval) {

// Define pseudo-instance properties from args self.callback = paramCallback; self.interval = paramInterval;

// Set flow control booleans self.isPaused = false; self.isComplete = false;

// Set default value for id   self.identify = -1;

// Begin first iterate and define id   self.iterate;

// Return for definition to local variable return self; };

/**  * @description This internal method of the * function is used to cancel any ongoing editing operation by clearing the * current timeout and setting the  flow control *  to true. This lets external handlers know that the * editing operation is complete, enabling or disabling them in turn. *  * @returns {void} */ main.setDynamicTimeout.cancel = function  { this.isComplete = true; wk.clearTimeout(this.identify); };

/**  * @description This internal method of the * function is used to pause any ongoing editing operation by setting the *  flow control   and clearing the * current  identified. This is  * called whenever the user presses the   modal button. *  * @returns {void} */ main.setDynamicTimeout.pause = function  { if (this.isPaused || this.isComplete) { return; }

this.isPaused = true; wk.clearTimeout(this.identify); };

/**  * @description This internal method of the * function is used to resume any ongoing and paused editing operation by  * setting the   flow control   to   *   and calling the   method to proceed * to the next iteration. It is called when the user presses "Resume." *  * @returns {void} */ main.setDynamicTimeout.resume = function  { if (!this.isPaused || this.isComplete) { return; }

this.isPaused = false; this.iterate; };

/**  * @description This internal method of the * function is used to proceed on to the next iteration by resetting the *  function property to the value returned by a new *  invocation. The function accepts as an optional * parameter an interval rate greater than that defined as in the function * instance property  for cases of ratelimiting. In such * a case, the rate is extended to 35 seconds before the callback is called. *  * @param {number} paramInterval - Optional interval rate parameter * @returns {void} */ main.setDynamicTimeout.iterate = function (paramInterval) { if (this.isPaused || this.isComplete) { return; }

// Interval should only be greater than instance interval paramInterval = (paramInterval < this.interval || paramInterval == null) ? this.interval : paramInterval;

// Define the identifier this.identify = wk.setTimeout(this.callback, paramInterval); };

/****************************************************************************/ /*                           Prototype Quicksort                            */ /****************************************************************************/

/**  * @description This implementation of the classic Quicksort algorithm is used * to quickly sort through a listing of category or namespace member pages * prior to iteration or display. Originally, the author went with the default *  native code. However, after running speed * tests between Chrome's V8 native implementation and a few custom Quicksort * algorithms, the author decided to go with a custom implementation. Current * speed tests for the native code generally result in sorting times of  * 700-900 ms for an array of 1,000,000  s, while this * custom implementation averages between 150-350 ms for the same data set. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left index * @param {number} paramRight - Parameter right index * @returns {Array } paramArray - Sorted array */ main.sort = function self (paramArray, paramLeft, paramRight) {

// Declarations var args, index;

// Convert to proper array args = Array.prototype.slice.call(arguments);

// Failsafe to ensure all parameters have initial values if (args.length === 1 && this.isThisAn("Array", args[0])) { paramArray = args[0]; paramLeft = 0; paramRight = paramArray.length - 1; }

// Recursively partition and call self until sorted if (paramArray.length) { index = self.partition(paramArray, paramLeft, paramRight);

if (paramLeft < index - 1) { self(paramArray, paramLeft, index - 1); }

if (index < paramRight) { self(paramArray, index, paramRight); }   }

return paramArray; };

/**  * @description One of two main helper functions of the Quicksort algorithm, *  is used, as the name implies, to swap the * elements included in the parameter array at the indices specified by  *   and. A temporary local * variable,, is used to faciliate the switch and store * the left pointer's value while the element at the right index is assigned * as the new left pointer's value. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left array index * @param {number} paramRight - Parameter right array index * @returns {void} */ main.sort.swap = function (paramArray, paramLeft, paramRight) {

// Declaration var swapped;

// Temporarily store left pointer's value swapped = paramArray[paramLeft];

// Set right pointer's value as new value at left index paramArray[paramLeft] = paramArray[paramRight];

// Former left value now set to the right paramArray[paramRight] = swapped; };

/**  * @description The second of two such helper functions of the Quicksort * algorithm, ,as its name implies, is used to   * divide the parameter array based on the values of the * index pointers. It is called often during 's set of   * divide-and-conquer recursive calls to further adjust the pointer values and * swap values accordingly while the left pointer value is less than the right * pointer index. *  * @param {Array } paramArray - Array of  s   * @param {number} paramLeft - Parameter left index * @param {number} paramRight - Parameter right index * @returns {number} leftPointer - Leftmost pointer index */ main.sort.partition = function (paramArray, paramLeft, paramRight) {

// Declarations var pivot, leftPointer, rightPointer;

// Middlemost pivot element pivot = paramArray[Math.floor((paramLeft + paramRight) / 2)];

// Initial pointer definitions leftPointer = paramLeft; rightPointer = paramRight;

while (leftPointer <= rightPointer) {

// Adjust left pointer index while (paramArray[leftPointer] < pivot) { leftPointer++; }

// Adjust right pointer index while (paramArray[rightPointer] > pivot) { rightPointer--; }

// Switch the elements at the present point indices if (leftPointer <= rightPointer) { this.swap(paramArray, leftPointer++, rightPointer--); }   }

// For use as "index" local var in main.sort return leftPointer; };

/****************************************************************************/ /*                        Prototype API methods                             */ /****************************************************************************/

/**  * @description As the name of the method implies, this Nirvana function is   * used to return data related to the user indicated by the parameter * . It's perhaps important to note that the method will * return data about the user making the request if improper title characters * are used in the query, making it important to check s   * with   or     * prior to invocation. *  * @param {string} paramUsername - A   username * @returns {object} -  resolved promise */ main.getUsernameData = function (paramUsername) { return nv.getJson("UserProfilePage", "renderUserIdentityBox", {     title: paramUsername,    }); };

/**  * @description One of two such methods, this function is used to post an   * individual thread to the selected user's message wall. Returning a resolved *  promise, the function provides the data for testing and * logging purposes on a successful edit and returns the associated error if  * the operation was unsuccessful. This function is called from within the * main submission handler 's assorted *  handlers if message walls are enabled * on-wiki. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.postMessageWallThread = function (paramConfig) { return nv.postJson("WallExternal", "postNewMessage", $.extend(false, { token: mw.user.tokens.get("editToken"), pagenamespace: 1200, }, paramConfig)); };

/**  * @description The second such method, this function is responsible for * posting a new talk topic to the talk page of the selected user. Like the * function above, it returns a resolved  promise and * provides the data for testing and logging purposes on success and the * associated error on failed operations. It too is called from within the * main submission handler 's assorted *  handlers if message walls are not enabled * on the wiki in question. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.postTalkPageTopic = function (paramConfig) { return $.ajax({     type: "POST",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "edit", section: "new", format: "json", }, paramConfig),   }); };

/**  * @description This function is one of two that handle the previewing of a   * formatted message, with this specific function used if message walls are * enabled on the wiki. Like all of the data request functions in the script, * it returns a resolved  promise that provides the * invoking handler  with the parsed * contents of the message in question on successful preview operations or the * associated error on failed operations. *  * @param {string} paramBody - Content of the message * @returns {object} -  object */ main.previewMessageWallThread = function (paramBody) { return nv.postJson("WallExternal", "preview", {     body: paramBody,    }); };

/**  * @description Like its matched function above, this function is used to   * handle previewing of messages on wikis that do not have message walls * enabled. Like the rest of the querying functions, this particular instance * returns a resolved  promise that provides the invoking * handler function, namely, with the * parsed contents of the message in question returned on successful * previewing operations or the relevant error on failed operations. *  * @param {string} paramBody Content of the message * @returns {object} -  object */ main.previewTalkPageTopic = function (paramBody) { return $.ajax({     type: "POST",      url: mw.util.wikiScript("index"),      data: {        action: "ajax",        rs: "EditPageLayoutAjax",        page: "SpecialCustomEditPage",        method: "preview",        content: paramBody,      }    }); };

/**  * @description This function queries the API for member pages of a specific * namespace, the id of which is included as a property of the parameter * . This argument is merged with the default *  parameter object and can sometimes include properties * related to  requests for additional members * beyond the default 5000 max. The method returns a resolved *  promise for use in attaching related callbacks to   * handle the member pages. *  * @param {object} paramConfig -   with varying properties * @returns {object} -  resolved promise */ main.getNamespaceMembers = function (paramConfig) { return $.ajax({     type: "GET",      url: mw.util.wikiScript("api"),      data: $.extend(false, { token: mw.user.tokens.get("editToken"), action: "query", list: "allpages", 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", cmprop: "title", cmdir: "desc", cmlimit: "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 - Either categories, loose pages, or namespaces * @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, user talks, and message walls get prefixes prefix = wk.wgFormattedNamespaces[{ categories: 14, namespaces: 0, }[paramType]] || "";

for (i = 0, n = paramEntries.length; i < n; i++) {

// Cache value to prevent multiple map lookups entry = this.capitalize(paramEntries[i].trim);

// If requires prefix but entry does not have prefix if (!this.startsWith(entry, prefix)) { entry = prefix + ":" + entry; }

// If legal page/category name, push into names array if (       (paramType !== "namespaces" && this.isLegalInput(entry)) ||        (paramType === "namespaces" && this.isInteger(entry))      ) { results.push(entry); } else { return $deferred.reject("logErrorSecurity"); }   }

if (!results.length) { // Error: No wellformed pages exist to edit $deferred.reject("logErrorNoWellformedPages"); } else { $deferred.resolve(results); }

return $deferred.promise; };

/**  * @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 = wk.wgFormattedNamespaces[ (this.utility.hasMessageWalls) ? 1200       : 3    ] + ":";

// "User:" userPrefix = wk.wgFormattedNamespaces[3] + ":";

// Array of extant usernames with prefix names = []; entries = [];

// Get wellformed, formatted namespace numbers or category names $getUsers = this.getValidatedEntries(paramEntries);

// Once acquired, apply to names array or pass along rejection message $getUsers.then(function (paramResults) {     names = paramResults;    }, $returnUsers.reject.bind($));

// Log paramResults if (DEBUG) { console.log(names); }

// Indicate checking is in progress $returnUsers.notify("logStatusCheckingUsernames");

// Iterate over provided list of usernames this.utility.timer = this.setDynamicTimeout(function {      if (counter === names.length) {

if (entries.length) { // Return Quicksorted entries return $returnUsers.resolve(this.sort(entries)).promise; } else { // Error: No wellformed pages exist to edit return $returnUsers.reject("logErrorNoWellformedUsernames").promise; }     }

// Acquire member pages of cat or ns     $getUser = $.when(this.getUsernameData(userPrefix + names[counter]));

// Once acquired, add pages to array $getUser.always($addUser.notify);

}.bind(this), this.interval);

/**    * @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 (DEBUG) {        console.log(paramResults, paramStatus, paramXHR);      }

if (paramStatus !== "success" || paramXHR.status !== 200) { $returnUsers.notify("logErrorNoUserData", names[counter++]); return this.utility.timer.iterate; }

if (paramResults.user && paramResults.user.edits !== -1) { $returnUsers.notify("logSuccessUserExists", names[counter]); entries.push(wallPrefix + names[counter++]); } else { $returnUsers.notify("logErrorNoSuchPage", names[counter++]); }

return this.utility.timer.iterate; }.bind(this));

return $returnUsers.promise; };

/**  * @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 or namespaces from which to  * acquire member pages. In such cases, a number of API calls are made * requesting the relevant members pages 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 or ns   * @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", }   }[paramType];

// Get wellformed, formatted namespace numbers or category names $getEntries = this.getValidatedEntries(paramEntries, paramType);

// Once acquired, apply to names array or pass along rejection message $getEntries.then(function (paramResults) {     names = paramResults;    }, $returnPages.reject.bind($));

// Iterate over user input entries this.utility.timer = this.setDynamicTimeout(function {      if (counter === names.length) {        $addPages.resolve;

if (entries.length) { // Return Quicksorted entries return $returnPages.resolve(this.sort(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 $returnPages.notify("logStatusFetchingMembers", names[counter]);

// Acquire member pages of cat or ns     $getPages = $.when(this[config.handler](parameters));

// Once acquired, add pages to array $getPages.always($addPages.notify);

}.bind(this), this.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 (DEBUG) {        console.log(paramResults, paramStatus, paramXHR);      }

if (paramStatus !== "success" || paramXHR.status !== 200) { $returnPages.notify("logErrorFailedFetch", names[counter++]); return this.utility.timer.iterate; }

// Define data data = paramResults.query[config.query];

// If page doesn't exist, add log entry and continue to next iteration if (data == null || data.length === 0) { $returnPages.notify("logErrorNoSuchPage", names[counter++]); return this.utility.timer.iterate; }

// Add extant page titles to the appropriate submission property for (i = 0, n = data.length; i < n; i++) { entries.push(data[i].title); }

// Only iterate counter if current query has no more extant pages if (       paramResults["query-continue"] ||        paramResults.hasOwnProperty("query-continue")      ) { parameters[config.continuer] = paramResults["query-continue"][config.query][config.continuer]; } else { parameters = {}; counter++; }

// On to the next iteration return this.utility.timer.iterate; }.bind(this));

return $returnPages.promise; };

/****************************************************************************/ /*                      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 = wk.parseInt(paramIndex, 10) || 0;

// Listing of selectable dropdown options options = "";

// Prefix used in title and default dropdown option prefix = "modal" + this.capitalize(paramName);

// Message for span title titleMessage = this.i18n.msg(prefix).escape;

// Assemble array of HTML option strings for (i = 0, n = paramValues.length; i < n; i++) {

// Sanitize parameter value = paramValues[i].toLowerCase;

// Option-specific message optionMessage = prefix + this.capitalize(value);

// Attributes for option element attributes = { value: value, };

// Choose which element to list as selected if (i === selectedIndex) { attributes.selected = "selected"; }

options += this.assembleElement(       ["option", attributes,          this.i18n.msg(optionMessage).escape,        ]      ); }

return this.assembleElement(     ["div", {class: this.Selectors.CLASS_CONTENT_DIV},        ["span", {class: this.Selectors.CLASS_CONTENT_SPAN},          titleMessage,        ],        ["select", {          size: "1",          name: paramName,          id: this.Selectors["ID_CONTENT_" + paramName.toUpperCase],          class: this.Selectors.CLASS_CONTENT_SELECT,        },          options,        ],      ]    ); };

/****************************************************************************/ /*                        Prototype Modal methods                           */ /****************************************************************************/

// Utility methods

/**  * @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.utility.timer && !this.utility.timer.isComplete) { this.utility.timer.cancel; }

// Add log message if i18n parameters passed if (arguments.length) { this.addModalLogEntry(Array.prototype.slice.call(arguments)); }

// Reset the form $("#" + this.Selectors.ID_CONTENT_FORM)[0].reset;

// Re-enable modal buttons and fieldset this.toggleModalComponentsDisable(false); };

/**  * @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, contents, $byline, $messaging, $modal,isMessaging;

// Definitions $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val; $messaging = $("#" + this.Selectors.ID_CONTENT_MESSAGE); $modal = $("#" + this.Selectors.ID_MODAL_CONTAINER + " > section"); isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2);

// Ensure messaging scene is shown if (!isMessaging || !this.modal.modal) { return; }

// Preview modal scene contents contents = ((this.modal.preview != null)     ? this.modal.scenes.preview      : this.modal.scenes.preview = this.buildPreviewContent    ).replace("$1", $byline).replace("$2", paramBody);

// Log preview HTML if (DEBUG) { console.log("Preview contents: ", contents); }

// Hide the messaging rather than reset the modal contents $messaging.hide;

// Add the preview container $modal.append(contents); };

// Modal methods

/**  * @description As with the preview-specific function above, namely *, this function serves the purposes of   * ensuring that all interactive elements in the various modal scenes are * provided their appropriate listeners. This function handles the disabling * of various components based on the actions performed and invokes *  for various elements that may * have wikitext link content on each scene change. *  * @returns {void} */ main.attachModalEvents = function  {

// Declarations var i, n, field, elements;

// Define elements to linksuggest elements = [ "ID_CONTENT_TARGET",   // Target replacement content "ID_CONTENT_CONTENT",  // New content to be added "ID_CONTENT_SUMMARY",  // Edit summary "ID_CONTENT_BODY",     // Message body ];

// Apply linksuggest to each element on focus event for (i = 0, n = elements.length; i < n; i++) { field = "#" + this.Selectors[elements[i]];

$(document).on("focus", field, $.prototype.linksuggest.bind($(field))); }

// Disable certain components this.toggleModalComponentsDisable(false); };

/**  * @description As with the similar preview-specific function *, this method builds a     * HTML framework to which will be added scene-specific element selectors and * body content. As with, this content is   * only created once within the body of  , its * value cached in a local variable for use with all scenes requiring * assembly and used in conjunction with * to make scene-specific adjustments. *  * @returns {string} - Assembled HTML string framework */ main.buildModalContent = function  { return this.assembleElement(     ["section", {        id: "$1",        class: this.Selectors.CLASS_CONTENT_CONTAINER,      },        ["form", {          id: this.Selectors.ID_CONTENT_FORM,          class: this.Selectors.CLASS_CONTENT_FORM,        },          ["fieldset", {id: this.Selectors.ID_CONTENT_FIELDSET},            "$2",            "$3",          ],          ["hr"],        ],        ["div", {class: this.Selectors.CLASS_CONTENT_DIV},          ["span", {class: this.Selectors.CLASS_CONTENT_SPAN},            this.i18n.msg("modalLog").escape,          ],          ["div", {            id: this.Selectors.ID_CONTENT_LOG,            class: this.Selectors.CLASS_CONTENT_DIV,          }],        ],      ]    ); };

/**  * @description This function is used to assemble the four main so-called * "scenes" that make up the body content of the  instance. * Originally made up of four methods, each handling its respective scene, * this method handles the creation of all scene  HTML * during  assembly, ensuring that all content is ready for * display on the user's interaction with the module dropdown menu. The * assembled contents are then stored in the  class instance * variable  for use throughout the script without the need * to reassemble their contents each time the scene changes. *

*

* Unfortunately, the function runs at O(n ^ 3) time in the assembly of all * required scenes. The author has considered revamping this function and only * creating scenes when the user requests a scene change by means of the * aforementioned scene dropdown menu, caching each new scene once created in  * the relevant instance variable object. *  * @returns {object} this.modal.scenes - Reference to instance variable prop */ main.buildModalScenes = function  {

// Declarations var i, j, k, m, n, o, framework, scene, defaultArgs, dropdownArgs, data, elements, object, arrays;

// Define scenes object this.modal.scenes = {};

// Default arguments defaultArgs = ["scene", this.Scenes];

data = [ [ // Replace {         handler: "assembleDropdown", parameterArrays: [ ["type", ["pages", "categories", "namespaces"]], ["case", ["sensitive", "insensitive"]], ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["target", "textarea"], ["indices", "input"], ["content", "textarea"], ["pages", "textarea"], ["summary", "input"], ]       }      ],      [ // Addition {         handler: "assembleDropdown", parameterArrays: [ ["action", ["prepend", "append"]], ["type", ["pages", "categories", "namespaces"]] ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["content", "textarea"], ["pages", "textarea"], ["summary", "input"], ]       }      ],      [ // Messaging {         handler: "assembleTextfield", parameterArrays: [ ["pages", "textarea"], ["byline", "input"], ["body", "textarea"], ]       }      ],      [ // Listing {         handler: "assembleDropdown", parameterArrays: [ ["type", ["categories", "namespaces"]] ]       },        {          handler: "assembleTextfield", parameterArrays: [ ["pages", "textarea"], ["members", "textarea"], ]       }      ]    ];

// Basic modal form framework framework = this.buildModalContent;

for (i = 0, n = this.Scenes.length; i < n; i++) {

// Internal definitions scene = this.Scenes[i];

// New scene object this.modal.scenes[scene] = {};

// Make copy of defaults and add the index dropdownArgs = $.merge($.merge([], defaultArgs), [i]);

// New defaultArgs object logged if (DEBUG) { console.log(dropdownArgs); }

// Init string HTML elements = "";

// Make it O(n^3) - go big or go home for (j = 0, m = data[i].length; j < m; j++) { object = data[i][j]; arrays = object.parameterArrays; for (k = 0, o = arrays.length; k < o; k++) { elements += this[object.handler].apply(this, arrays[k]); }     }

// Make use of modal framework to insert scene-specific HTML this.modal.scenes[scene] = framework .replace("$1", this.Selectors["ID_CONTENT_" + scene.toUpperCase]) .replace("$2", this.assembleDropdown.apply(this, dropdownArgs)) .replace("$3", elements); }

// Log instance variable modal's scenes property if (DEBUG) { console.log("modal.scenes:", this.modal.scenes); }

// Return reference for use in buildModal return this.modal.scenes; };

/**  * @description This method is used to create a new * instance that serves as the primary interface of the script. It sets all * click events, defines all modal  buttons in the modal, * and assembles all the so-called "scenes" related to the various operations * supported by MassEdit. Originally, this function also injected the modal * CSS styling prior to creation of the modal, though for the purposes of  * ensuring single responsibility for all functions, the styling was moved * into a separate function, namely. *  * @returns {object} - A new   instance */ main.buildModal = function  { return new wk.dev.modal.Modal({     content: this.buildModalScenes[this.Scenes[0]], // 1st scene is default      id: this.Selectors.ID_MODAL_CONTAINER,      size: "medium",      title: this.i18n.msg("buttonScript").escape,      events: {        submit: this.handleSubmit.bind(this),        toggle: this.handleToggle.bind(this),        preview: this.handlePreviewing.bind(this),        clear: this.handleClear.bind(this),        cancel: this.handleCancel.bind(this),      },      buttons: [        {          text: this.i18n.msg("buttonSubmit").escape,          event: "submit",          primary: true,          id: this.Selectors.ID_MODAL_SUBMIT,          classes: [            this.Selectors.CLASS_MODAL_BUTTON,            this.Selectors.CLASS_MODAL_OPTION,          ],        },        {          text: this.i18n.msg("buttonPause").escape,          event: "toggle",          primary: true, disabled: true, id: this.Selectors.ID_MODAL_TOGGLE, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_TIMER, ],       },        {          text: this.i18n.msg("buttonCancel").escape, event: "cancel", primary: true, disabled: true, id: this.Selectors.ID_MODAL_CANCEL, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_TIMER, ],       },        {          text: this.i18n.msg("buttonPreview").escape, event: "preview", primary: true, disabled: true, id: this.Selectors.ID_MODAL_PREVIEW, classes: [ this.Selectors.CLASS_MODAL_BUTTON, ],       },        {          text: this.i18n.msg("buttonClose").escape, event: "close", id: this.Selectors.ID_MODAL_CLOSE, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_LEFT, this.Selectors.CLASS_MODAL_OPTION, ],       },        {          text: this.i18n.msg("buttonClear").escape, event: "clear", id: this.Selectors.ID_MODAL_CLEAR, classes: [ this.Selectors.CLASS_MODAL_BUTTON, this.Selectors.CLASS_MODAL_LEFT, this.Selectors.CLASS_MODAL_OPTION, ],       },      ],    });  };

/**  * @description This method is the primary mechanism by which the modal is   * displayed to the user. If the modal has not been previously assembled, the * function constructs a new  instance via an invocation of   * , creates the modal, and attaches all the requisite * event listeners related to enabling  and find-and- * replace-specific modal elements (linksuggest is enabled for the content  *   and the edit summary  ). *

*

* Once all listeners have been attached, the new modal is displayed to the * user. If the modal has been assembled prior to method invocation, the * instance is displayed to the user and the method exits. *  * @returns {void} */ main.displayModal = function  { if (this.modal.modal != null) { this.modal.modal.show; return; }

// Apply modal CSS styles prior to creation this.injectModalStyles;

// Construct new Modal instance this.modal.modal = this.buildModal;

// Create, then apply all relevant listeners this.modal.modal.create.then(function {

// Apply initial linksuggest this.attachModalEvents;

// Change scene depending on user needs $(document).on("change", "#" + this.Selectors.ID_CONTENT_SCENE,       this.handleClear.bind(this, true));

// Once events are set, display the modal this.modal.modal.show; }.bind(this));

// Log modal instance variable if (DEBUG) { console.log("this.modal: ", this.modal); } };

/****************************************************************************/ /*                      Prototype Event handlers                            */ /****************************************************************************/

/**  * @description Arguably the most important method of the program, this * function coordinates the entire mass editing process from the initial press * of the "Submit" button to the conclusion of the editing operation. The * entire workings of the process were contained within a single method to  * assist in maintaining readability when it comes time to invariably repair * bugs and broken functionality. The other two major methods used by this * function are  and *, both of which are used to sanitize input * and return wellformed loose member pages if applicable. *

*

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

*

* As of the latest update implementing additional editing functionality, this * method was temporarily separated into four separate methods related to each * of the four scenes. However, as the same progression of sanitizing input, * acquiring pages, iterating over pages, and logging accordingly was * evidenced in all operations, these handlers were once again merged into * this single method to prevent copious amounts of copy/pasta. The only * operation that exits early and does not iterate is the listing operation, * which merely acquires lists of category members and prepends them to an  * element. *  * @returns {void} */ main.handleSubmit = function  { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }

// Declarations var $action, $type, $case, $content, $target, $indices, indices, $pages, pages, $byline, $summary, counter, config, data, pageIndex, newText, $getPages, $postPages, $getNextPage, $getPageContent, $postPageContent, error, $scene, isCaseSensitive, isReplace, isAddition, isMessaging, isListing, $members, $selected, $body, pagesType;

// Dropdowns $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $action = $("#" + this.Selectors.ID_CONTENT_ACTION)[0]; $type = $("#" + this.Selectors.ID_CONTENT_TYPE)[0]; $case = $("#" + this.Selectors.ID_CONTENT_CASE)[0];

// Textareas/inputs $target = $("#" + this.Selectors.ID_CONTENT_TARGET).val; $indices = $("#" + this.Selectors.ID_CONTENT_INDICES).val; $content = $("#" + this.Selectors.ID_CONTENT_CONTENT).val; $pages = $("#" + this.Selectors.ID_CONTENT_PAGES).val; $summary = $("#" + this.Selectors.ID_CONTENT_SUMMARY).val;

// For acquiring text of selected option $selected = $("#" + this.Selectors.ID_CONTENT_TYPE + " option:selected");

// Messaging exclusives $body = $("#" + this.Selectors.ID_CONTENT_BODY).val; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val;

// Listing exclusive $members = $("#" + this.Selectors.ID_CONTENT_MEMBERS);

// Cache frequently used boolean flags isReplace = ($scene.value === "replace" && $scene.selectedIndex === 0); isAddition = ($scene.value === "add" && $scene.selectedIndex === 1); isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2); isListing = ($scene.value === "list" && $scene.selectedIndex === 3);

// Substitute for $1 in logErrorNoPages pagesType = ((isMessaging)     ? this.i18n.msg("modalTypeUsernames").escape      : $selected.text).toLowerCase;

// If no scene selected (should not happen) if (!isReplace && !isAddition && !isMessaging && !isListing) { return;

// Is not in the proper rights group } else if (!isListing && !this.hasRights(isMessaging)) { this.resetModal; this.addModalLogEntry("logErrorUserRights"); return;

// Is either append/prepend with no content input included } else if (isAddition && !$content) { this.addModalLogEntry("logErrorNoContent"); return;

// Is find-and-replace with no target content included } else if (isReplace && !$target) { this.addModalLogEntry("logErrorNoTarget"); return;

// No pages included } else if (!$pages) { this.addModalLogEntry("logErrorNoPages", pagesType); return;

// If edit summary is greater than permitted max of 800 characters } else if ($summary && $summary.length > this.Utility.MAX_SUMMARY_CHARS) { this.addModalLogEntry("logErrorOverlongSummary"); return;

// If message title is not legal } else if (isMessaging && !this.isLegalInput($byline)) { this.addModalLogEntry("logErrorSecurity"); return;

// If no message body is included } else if (isMessaging && !$body) { this.addModalLogEntry("logErrorMissingBody"); return; }

// Status log message, scene-dependent this.addModalLogEntry(     (isReplace || isAddition)        ? "logStatusEditing"        : (isMessaging)          ? "logStatusMessaging"          : "logStatusGenerating"    ); this.toggleModalComponentsDisable(true);

// Find-and-replace specific variable definitions if (isReplace) {

// Only wellformed integers should be included as f-n-r indices indices = $indices.split(",").map(function (paramEntry) {       if (this.isInteger(paramEntry.trim)) {          return wk.parseInt(paramEntry, 10);        }      }.bind(this)).filter(function (paramEntry) {        return paramEntry != null; // Avoid cases of [undefined]      });

// Whether not search and replace is case sensitive isCaseSensitive = ($case.selectedIndex === 0 &&       $case.value === "sensitive"); }

// Array of pages/categories/namespaces pages = $pages.split(/[\n]+/).filter(function (paramEntry) {     return paramEntry !== "";    });

// Page counter for setInterval counter = 0;

// Default page editing parameters config = {};

// New pending status Deferreds $postPages = new $.Deferred; $getNextPage = new $.Deferred;

// Log flag for inspection if (DEBUG) { console.log("hasMessageWalls: ", this.utility.hasMessageWalls); }

/**    * @description The     is used * to acquire either wellformed loose page titles or the titles of member * pages belonging to input categories or namespaces. In cases of user * messaging, the function makes use of      * functionality to determine whether the wiki on which the script is being * used has enabled message walls. This knowledge is required as the prefix * applied to username input will differ ("Message Wall:" vs "User talk:") * accordingly. Similar functionality can be glimpsed in    *. */   $getPages = new $.Deferred(function ($paramOuter) {      new $.Deferred(function ($paramInner) { (!isMessaging || this.utility.hasMessageWalls != null) ? $paramInner.resolve.promise : wk.wgMessageWallsExist.then(             function  {                return $paramInner.resolve(true).promise;              }.bind(this),

function { return $paramInner.resolve(false).promise; }.bind(this) );     }.bind(this)).then(        function (paramHasWalls) {          if (paramHasWalls != null) {            this.utility.hasMessageWalls = paramHasWalls;          }

// Get list of wellformed pages/usernames or member pages return this[ (isListing || (!isMessaging && $type.value !== "pages")) ? "getMemberPages" : (isMessaging) ? "getExtantUsernames" : "getValidatedEntries" ](pages, ($type != null) ? $type.value : null); }.bind(this) ).then( $paramOuter.resolve.bind($), // $getPages.done $paramOuter.reject.bind($), // $getPages.fail $paramOuter.notify.bind($)  // $getPages.progress );   }.bind(this));

/**    * @description The resolved   returns an array of     * loose pages from a namespace or category, or returns an array of checked * loose pages if the individual pages option is selected or usenames are * inputted. Once resolved, assuming the user is not simply generating a    * pages list,   uses a       * to iterate over the pages, optionally acquiring page content for * find-and-replace. Once done, an invocation of  calls *  to assemble the parameters needed to     * edit the page in question. Once all pages have been edited, the pending * s are resolved and the timer exited. */   $getPages.done(function (paramResults) {      pages = paramResults;

// Log pages list (members or wellformed pages) if (DEBUG) { console.log("$getPages: ", pages); }

// Listing activities end once members are acquired and shown to the user if (isListing) { // Add category members to textarea $members.text(pages.join("\n"));

$getNextPage.resolve; $postPages.resolve("logSuccessListingComplete"); return; }

// Iterate over pages this.utility.timer = this.setDynamicTimeout(function {        if (counter === pages.length) {          $getNextPage.resolve;          $postPages.resolve("logSuccessEditingComplete");        } else {          $getPageContent = (!isReplace)            ? new $.Deferred.resolve({}).promise            : this.getPageContent(pages[counter]);

// Grab data, extend parameters, then edit the page $getPageContent.always($postPages.notify); }     }.bind(this), this.interval);    }.bind(this));

/**    * @description In the cases of failed loose page acquisitions, either from * a failed API GET request or from a lack of wellformed input loose pages, * the relevant log entry returned from the getter function's    *   is logged, the timer canceled, and the modal form * re-enabled by means of. */   $getPages.fail(this.resetModal.bind(this));

/**    * @description Whenever the getter function (  or     *  ) needs to notify its invoking function * of a new ongoing category/namespace member acquisition operation, the * returned status message is acquired and added to the modal log. */   $getPages.progress(this.addModalLogEntry.bind(this));

/**    * @description Once the     is     * resolved, indicating the completion of the requested mass edits, a final * status message is logged, the form reenabled and reset for a new * round, and the  timer canceled by means of     *. */   $postPages.always(this.resetModal.bind(this));

/**    * @description The   handler is used to extend the *  object with properties relevant to the action * being performed (i.e. addition, replace, or messaging). Once complete, * the modified page content is committed and the edit made by means of    * a scene-specific handler, namely either  , *, or. * Once the edit is complete and a resolved promise returned, *  pings the pending *  to log the relevant messages and iterate on to     * the next page to be edited. */   $postPages.progress(function (paramResults) {      if (DEBUG) {        console.log("$postPages results: ", paramResults);      }

// Addition parameters if (isAddition) { config = { handler: "postPageContent", parameters: { title: pages[counter], token: mw.user.tokens.get("editToken"), summary: $summary, }       };

// "appendtext" or "prependtext" config.parameters[$action.value.toLowerCase + "text"] = $content;

// Find-and-replace parameters } else if (isReplace) { pageIndex = Object.keys(paramResults.query.pages)[0]; data = paramResults.query.pages[pageIndex];

// Return if page doesn't exist to the server if (pageIndex === "-1") { this.addModalLogEntry("logErrorNoSuchPage", pages[counter++]); return this.utility.timer.iterate; }

config = { handler: "postPageContent", parameters: { title: pages[counter], text: data.revisions[0]["*"], basetimestamp: data.revisions[0].timestamp, startimestamp: data.starttimestamp, token: data.edittoken, summary: $summary, }       };

// Replace instances of chosen text with inputted new text newText = this.replaceOccurrences(config.parameters.text,         isCaseSensitive, $target, $content, indices);

// Return if old & new revisions are identical in content if (newText === config.parameters.text) { // Error: No instances of $1 found in $2. this.addModalLogEntry("logErrorNoMatch", $target, pages[counter++]); return this.utility.timer.iterate; } else { config.parameters.text = newText; }

// Messaging parameters } else if (isMessaging) { config = [ {           handler: "postTalkPageTopic", parameters: { sectiontitle: $byline, text: $body, title: pages[counter], }         },          {            handler: "postMessageWallThread", parameters: { messagetitle: $byline, body: $body, pagetitle: pages[counter], }         },        ][+this.utility.hasMessageWalls]; }

// Log all config handlers and parameters if (DEBUG) { console.log("Config: ", config); }

// Deferred attached to posting of data $postPageContent = this[config.handler](config.parameters); $postPageContent.always($getNextPage.notify);

}.bind(this));

/**    * @description The pending state *  is pinged by   once * an POST request is made and a resolved status * returned. The  callback takes the resultant success/ * failure data and logs the relevant messages before moving the operation * on to the iteration of the  timer. If the * user has somehow been ratelimited, the function introduces a 35 second * cooldown period before undertaking the next edit and pushes the unedited * page back onto the  stack. */   $getNextPage.progress(function (paramData) {      if (DEBUG) {        console.log("$getNextPage results: ", paramData);      }

error = (paramData.error && paramData.error.code) ? paramData.error.code : "unknownerror";

// Success differs depending on status of message walls on wiki if (       ( (!isMessaging || (isMessaging && !this.utility.hasMessageWalls)) && paramData.edit && paramData.edit.result === "Success" ) ||       (          isMessaging && this.utility.hasMessageWalls && paramData.status )     ) {        this.addModalLogEntry("logSuccessEditing", pages[counter++]); } else if (error === "ratelimited") {

// Show ratelimit message with the delay in seconds this.addModalLogEntry("logErrorRatelimited",         (this.Utility.DELAY / 1000).toString);

// Push the unedited page back on the stack pages.push(pages[counter++]); } else { // Error: $1 not edited. Please try again. this.addModalLogEntry("logErrorEditing", pages[counter++]); }

// On to the next iteration this.utility.timer.iterate(       (error === "ratelimited")          ? this.Utility.DELAY          : null      ); }.bind(this)); };

/**  * @description This function serves as the primary event listener for presses * of the "Preview" button available to users who are seeking to mass-message * other users. From the end user's perspective, the button press should be  * met with the fading out of the messaging modal scene and the display of a   * parsed version of the title and associated message. On presses of the close * button, the preview should fade out and be replaced by the messaging modal * with all of the user's messaging input still displayed in the textfields. *

*

* This function accomplishes this by checking the user's input and querying * the database via  to determine whether the * wiki on which the script is being used has enabled message walls. Depending * on this query, the methods used to render and display the preview will * change though the end results will be the same. The function will fade in  * and out using   and invoke the requisite *  to show the preview   and *  to handle collapsibles and other events. *  * @returns {void} */ main.handlePreviewing = function  { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }

// Declarations var $scene, $byline, $body, isMessaging, $previewMessage;

// Definitions $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0]; $byline = $("#" + this.Selectors.ID_CONTENT_BYLINE).val; $body = $("#" + this.Selectors.ID_CONTENT_BODY).val; isMessaging = ($scene.value === "message" && $scene.selectedIndex === 2);

// Just in case... if (!isMessaging) { return; }

// Check for title if (!$byline) { this.addModalLogEntry("logErrorMissingByline"); return;

// Check for body content } else if (!$body) { this.addModalLogEntry("logErrorMissingBody"); return; }

/**    * @description   handles the acquisition of     * parsed HTML content related to the user's input message body and title. * Naturally, in order to ensure that the proper API methods are invoked, * the script must determine if the wiki on which MassEdit is being used * has enabled message walls. Once the proper method has been invoked, the * resultant  containing the message body HTML is passed * to. */   $previewMessage = new $.Deferred(function ($paramOuter) {      new $.Deferred(function ($paramInner) { (this.utility.hasMessageWalls != null) ? $paramInner.resolve(this.utility.hasMessageWalls).promise : wk.wgMessageWallsExist.then(             function  {                return $paramInner.resolve(true).promise;              }.bind(this),

function { return $paramInner.resolve(false).promise; }.bind(this) );     }.bind(this)).then(        function (paramHasWalls) {          if (this.utility.hasMessageWalls == null) {            this.utility.hasMessageWalls = paramHasWalls;          }

return this[(paramHasWalls) ? "previewMessageWallThread" : "previewTalkPageTopic" ]($body); }.bind(this) ).then( $paramOuter.resolve.bind($), // $previewMessage.done $paramOuter.reject.bind($)  // $previewMessage.fail );   }.bind(this));

/**    * @description Upon successful completion of the preview request operation, * the results are logged and the  scene transition * method invoked. In such cases,  is invoked * following the fade out to hide the messaging scene and append a preview *  and   is invoked * following the post appending fade-in. */   $previewMessage.done(function (paramResults) {      if (DEBUG) {        console.log("$previewMessage results: ", paramResults);      }

// Bypass handleClear's default functionality via the functions object this.handleClear({       before: this.displayPreview.bind(this, paramResults[ (this.utility.hasMessageWalls) ? "body" : "html"]),       after: this.attachPreviewEvents.bind(this),      }); }.bind(this));

/**    * @description In cases wherein the message preview has failed for some * reason, the message scene doesn't change. The only alteration is the * addition of a relevant status log message denoting a failed preview * request. */   $previewMessage.fail(this.addModalLogEntry.bind(this, "logErrorNoPreview")); };

/**  * @description The   is the primary click handler for * the "Pause/Resume" button used to toggle the iteration timer. Depending on  * whether or not the timer is in use in iterating through collated pages * requiring editing, the text of the button will change accordingly. Once * invoked, the method will either restart the timer during an iteration or  * pause it indefinitely. If the timer is not running, the method will exit. *  * @returns {void} */ main.handleToggle = function  { if (     !this.utility.timer ||      (this.utility.timer && this.utility.timer.isComplete)    ) { if (DEBUG) { console.dir(this.utility.timer); }     return; }

// Declarations var $toggle, config;

// Definitions $toggle = $("#" + this.Selectors.ID_MODAL_TOGGLE); config = [ {       message: "logTimerPaused", text: "buttonResume", method: "pause", },     {        message: "logTimerResume", text: "buttonPause", method: "resume", }   ][+this.utility.timer.isPaused];

// Add status log entry this.addModalLogEntry(config.message);

// Change the text of the button $toggle.text(this.i18n.msg(config.text).escape);

// Either resume or pause the setDynamicTimeout this.utility.timer[config.method]; };

/**  * @description Similar to , this function is used to   * cancel the timer used to iterate through pages requiring editing. As such, * it cancels the timer, adds a relevant status log entry, and re-enables the * standard editing buttons in the modal. If the timer is  * presently not running, the method simply returns and exits. The timer is  * logged in the console if   is set to. *  * @returns {void} */ main.handleCancel = function  { if (     !this.utility.timer ||      (this.utility.timer && this.utility.timer.isComplete)    ) { if (DEBUG) { console.dir(this.utility.timer); }     return; } else { this.resetModal("logTimerCancel"); } };

/**  * @description As the name implies, the   listener is   * mainly used to clear modal contents and reset the   HTML * element. Rather than simply invoke the helper function *, however, this function adds some animation by   * disabling the button set and fading in and out of the modal body during the * clearing operation, displaying a status message in the log upon completion. *

*

* In addition to its main responsibility of clearing the modal fields of  * content, the function is also used as the primary means of transitioning * between scenes on changes to the scene dropdown. It can even accept in  * place of a "transitioning"   input flag a dedicated * functions  containing handlers for the fade-in/fade-out * progression, bypassing all other internal functionality apart from the * core fade operation. *  * @param {boolean|object} paramInput - Flag or handler * @returns {void} */ main.handleClear = function (paramInput) { if (this.utility.timer && !this.utility.timer.isComplete) { if (DEBUG) { console.dir(this.utility.timer); }     return; }

// Declarations var $scene, functions, visible, hidden, isTransitioning;

// Whether the function is being used to reset or scene transition isTransitioning = (typeof paramInput !== "boolean") ? false : paramInput;

// Scene dropdown element $scene = $("#" + this.Selectors.ID_CONTENT_SCENE)[0];

// $.prototype.animate objects visible = {opacity: 1}; hidden = {opacity: 0};

// Define listeners for fade-in and fade-out (either custom or defaults) functions = (     this.isThisAn("Object", paramInput) &&      paramInput.hasOwnProperty("before") &&      paramInput.hasOwnProperty("after")    ) ? paramInput : [         { // Standard form clear before: this.resetModal.bind(this), after: this.addModalLogEntry.bind(this, "logSuccessReset") },         { // Scene transition before: this.modal.modal.setContent.bind(this.modal.modal,             this.modal.scenes[$scene.value]), after: this.attachModalEvents.bind(this), }       ][+isTransitioning];

// Disable all modal buttons for duration of fade and reset $("." + this.Selectors.CLASS_MODAL_BUTTON).prop("disabled", true);

// Fade out on modal and reset content before fade-in $("#" + this.Selectors.ID_MODAL_CONTAINER + " > section") .animate(hidden, this.Utility.FADE_INTERVAL, functions.before) .animate(visible, this.Utility.FADE_INTERVAL, functions.after); };

/****************************************************************************/ /*                     Prototype Pseudo-constructor                         */ /****************************************************************************/

/**  * @description The confusingly named   function serves * as a pseudo-constructor of the MassEdit class instance .Through the *  passed to  's invocation of   *   sets the  , *, and   instance properties, this * function sets default values for  and * and defines the toolbar element and its associated event listener, namely * .   *

*

* Following this function's invocation, the MassEdit class instance will have * a total of five instance variables, namely, , *,  ,  , and * . All other functionality related to MassEdit is stored * in the class instance prototype, the  namespace object, * for convenience. *  * @returns {void} */ main.init = function  {

// Declarations var $toolItem, toolText;

// I18n config for wiki's content language this.i18n.useContentLang;

// Initialize new modal property this.modal = {};

// New helper object for local config this.utility = {};

// Initialize a new dynamic timer object this.utility.timer = null;

// New instance boolean property this.utility.hasMessageWalls = null;

// View instance props and prototype if (DEBUG) { console.dir(this); }

// Text to display in the tool element toolText = this.i18n.msg("buttonScript").plain;

// Build tool item (nested link inside list element) $toolItem = $(this.assembleOverflowElement(toolText));

// Display the modal on click $toolItem.on("click", this.displayModal.bind(this));

// Either append or prepend the tool to the target $(this.placement.element)[this.placement.type]($toolItem); };

/****************************************************************************/ /*                         Setup Helper methods                             */ /****************************************************************************/

/**  * @description The first of two user input validators, this function is used * to ensure that the user's included config details related to Placement.js  * are wellformed and legitimate. MassEdit.js offers support for all of  * Placement.js's default element locations, though as a nod to the previous * incarnation of the script, the default placement element is the toolbar and * the default type is "append." In the event of an error being caught due to  * a malformed element location or a missing type, the default config options * housed in  are used instead to   * ensure that user input mistakes are handled somewhat gracefully. *  * @param {object} paramConfig - Placement.js-specific config * @returns {object} config - Adjusted Placement.js config */ init.definePlacement = function (paramConfig) {

// Declarations var config, loader;

// Definitions config = {}; loader = wk.dev.placement.loader;

try { config.element = loader.element(paramConfig.element); } catch (e) { config.element = loader.element(this.Placement.DEFAULTS.ELEMENT); }

try { config.type = loader.type(       (this.Placement.VALID_TYPES.indexOf(paramConfig.type) !== -1)          ? paramConfig.type          : this.Placement.DEFAULTS.TYPE      ); } catch (e) { config.type = loader.type(this.Placement.DEFAULTS.TYPE); }

// Set script name loader.script(this.Utility.SCRIPT);

return config; };

/**  * @description The second of the two validator functions used to check that * user input is wellformed and legitimate, this function checks the user's  * edit interval value against the permissible values for standard users and * flagged bot accounts. In order to ensure that the operations are carried * out smoothly, the user's rate is adjusted if it exceeds the edit * restrictions placed upon accounts of different user rights levels. The * original incarnation of this method came from a previous version of  * MassEdit which made use of a similar, jankier system to ensure the smooth * progression through all included pages without loss of required edits. *  * @see <a href="https://git.io/fA4Jk">SUS-4775</a> * @see <a href="https://git.io/fA4eQ">VariablesBase.php</a> * @param {number} paramInterval - User's input interval value * @return {number} - Adjusted interval */ init.defineInterval = function (paramInterval) { if (     wk.wgUserGroups.indexOf("bot") !== -1 &&      (paramInterval < this.Utility.BOT_INTERVAL || wk.isNaN(paramInterval))    ) { return this.Utility.BOT_INTERVAL; // Reset to max 80 edits/minute } else if (     wk.wgUserGroups.indexOf("user") !== -1 &&      (paramInterval < this.Utility.STD_INTERVAL || wk.isNaN(paramInterval))    ) { return this.Utility.STD_INTERVAL; // Reset to max 40 edits/minute } else { return wk.parseInt(paramInterval, 10); } };

/****************************************************************************/ /*                          Setup Primary methods                           */ /****************************************************************************/

/**  * @description The confusingly named   function is used * to coordinate the script setup madness in a single method, validating all * user input by means of helper method invocation and setting all instance * properties of the MassEdit class instance. Once the *   has been assembled containing the relevant instance * variables for placement, edit interval, and i18n messages, the method calls *  to construct a new MassEdit class instance, * passing the  and the   namespace *  as the instance's prototype. *

*

* The separation of setup code and MassEdit functionality code into distinct * namespace s helped to ensure that code was logically * organized per the single responsibility principle and more readable by  * virtue of the fact that each namespace handles distinctly different tasks. * This will assist in debugging should an issue arise with either the setup * or the script's functionality itself. *  * @param {object} paramLang - i18n   returned from hook * @returns {void} */ init.main = function (paramLang) {

// Declarations var i, n, array, descriptor, parameter, lowercase, method, property, descriptorProperties, configObject;

// Two of the three local instance variables array = ["Interval", "Placement"];

// Support both MassEdit config and legacy Message config configObject = wk.MassEditConfig || wk.configMessage || {};

if (DEBUG) { console.log("Config: ", configObject); }

// New Object.create descriptor object descriptor = {};

descriptorProperties = { enumerable: true, configurable: false, writable: false, };

// Set I18n object as instance property descriptor.i18n = $.extend(true, {}, descriptorProperties); descriptor.i18n.value = paramLang;

// Reduce copy pasta for (i = 0, n = array.length; i < n; i++) {

// Definitions property = array[i]; method = "define" + property; lowercase = property.toLowerCase; parameter =(configObject.hasOwnProperty(lowercase)) ? configObject[lowercase] : null;

// New descriptor entry descriptor[lowercase] = $.extend(true, {}, descriptorProperties);

// Define descriptor entry value descriptor[lowercase].value = this[method](parameter); }

// Log init object for inspection if (DEBUG) { console.dir(init); }

// Create new MassEdit instance Object.create(main, descriptor).init; };

/**  * @description This function is invoked as many times as there are external * dependencies, serving as the primary hook handler for each of the required * events denoted in. Once all * dependencies have been successfully loaded and the hooks fired, the * function loads I18n-js messages and invokes  as   * the callback function. *  * @returns {void} */ init.load = function  { if (++this.loaded === Object.keys(this.Dependencies.SCRIPTS).length) { wk.dev.i18n.loadMessages(this.Utility.SCRIPT, {       cacheVersion: this.Utility.CACHE_VERSION,      }).then(this.main.bind(this)); } };

/**  * @description This function is only invoked once the ResourceLoader has * successfully loaded the various required  core modules, * executing this callback on completion. This function is responsible for * assembling the relevant hook event aliases from the listing of hook names * included in  that denote the * required external dependencies and libraries required by the script. *  * @returns {void} */ init.preload = function  {

// Declarations var i, n, hooks;

// Definitions this.loaded = 0; hooks = Object.keys(this.Dependencies.SCRIPTS);

// Assemble all hooks and attach init.load as handler for (i = 0, n = hooks.length; i < n; i++) { mw.hook(hooks[i]).add(init.load.bind(this)); } };

// Load MW modules mw.loader.using(init.Dependencies.MODULES).then(init.preload.bind(init));

// Load Dev scripts (4x) wk.importArticles({   type: "script",    articles: Object.values(init.Dependencies.SCRIPTS),  }); });