User:Enterprisey/delsort.js

//<nowiki>( function ( $, mw ) {    mw.loader.load( "jquery.chosen" );    mw.loader.load( "mediawiki.ui.input", "text/css" );    var afdcCategories = { "m": "Media and music", "o": "Organization, corporation, or product", "b": "Biographical", "s": "Society topics", "w": "Web or Internet", "g": "Games or sports", "t": "Science and technology", "f": "Fiction and the arts", "p": "Places and transportation", "i": "Indiscernible or unclassifiable topic", "u": "Not sorted yet" };    var ADVERTISEMENT = " ([[User:Enterprisey/delsort|assisted]])";    var currentAfdcCat = "";    var currentDelsortCategories = [];    if ( mw.config.get( "wgPageName" ).indexOf("Wikipedia:Articles_for_deletion/") != -1 &&         mw.config.get( "wgPageName" ).indexOf("Wikipedia:Articles_for_deletion/Log/") == -1) {        var portletLink = mw.util.addPortletLink("p-cactions", "#", "Delsort", "pt-delsort", "Perform deletion sorting");        // Load list of delsort categories        var delsortCategoriesPromise = $.ajax( {            url: "https://en.wikipedia.org/w/index.php?action=raw&title=" + encodeURIComponent( "Wikipedia:WikiProject Deletion sorting/Computer-readable.json" ) + "&maxage=86400&smaxage=86400",            dataType: "json"        } )        $( portletLink ).click( function ( e ) {            e.preventDefault();            // Validation for new custom fields            var validateCustomCat = function ( container ) {                var categoryName = container.children( "input" ).first().val();                $.getJSON(                    mw.util.wikiScript("api"),                    {                        format: "json",                        action: "query",                        prop: "pageprops",                        titles: "Wikipedia:WikiProject Deletion sorting/" + categoryName                    }                ).done( function ( data ) {                    var setStatus = function ( status ) {                        var text = "Not sure";                        var imageSrc = "https://upload.wikimedia.org/wikipedia/commons/a/ad/Question_mark_grey.png";                        switch( status ) {                        case "d":                            text = "Doesn't exist";                            imageSrc = "https://upload.wikimedia.org/wikipedia/commons/5/5f/Red_X.svg";                            break;                        case "e":                            text = "Exists";                            imageSrc = "https://upload.wikimedia.org/wikipedia/commons/1/16/Allowed.svg";                            break;                        }                        container.children( ".category-status" ).empty()                            .append( $( "<img>", { "src": imageSrc,                                "style": "padding: 0 5px; width: 20px; height: 20px" } ) )                            .append( text );                    };                    if( data && data.query && data.query.pages ) {                        if( data.query.pages.hasOwnProperty( "-1" ) ) {                            setStatus( "d" );                        } else {                            setStatus( "e" );                        }                    } else {                        setStatus( "n" );                    }                } );            };            // Define a function to add a new custom field, used below            var addCustomField = function ( e ) {                $( "<div>" )                    .appendTo( "#delsort-td" )                    .css( "width", "100%" )                    .css( "margin", "0.25em auto" )                    .append( $( "<input>" )                             .attr( "type", "text" )                             .addClass( "mw-ui-input mw-ui-input-inline custom-delsort-field" )                             .change( function ( e ) {                                 validateCustomCat( $( this ).parent() );                             } ) )                    .append( $( "<span>" ).addClass( "category-status" ) )                    .append( " (" )                    .append( $( "<img>", { "src": "https://upload.wikimedia.org/wikipedia/commons/a/a2/Crystal_128_reload.svg",                        "style": "width: 15px; height: 15px; cursor: pointer" } )                             .click( function ( e ) {                                 validateCustomCat( $( this ).parent() );                             } ) )                    .append( ")" )                    .append( $( "<button>" )                             .addClass( "mw-ui-button mw-ui-destructive mw-ui-quiet" )                             .text( "Remove" )                             .click( function () {                                 $( this ).parent().remove();                             } ) );            };            $( "#mw-content-text" ).prepend('<div style="border: thin solid rgb(197, 197, 197); box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.25); border-radius: 3px; padding: 5px; position: relative;" id="delsort">' +'  <div id="delsort-title" style="font-size: larger; font-weight: bold; text-align: center;">Select a deletion sorting category</div>' +'  <table style="margin: 2em auto; border-collapse: collapse;" id="delsort-table">' +'    <tr style="font-size: larger"><th>AFDC</th><th>DELSORT</th></tr>' +'    <tr>' +'      <td style="padding-right: 10px;">' +'        <table id="afdc">' +'        </table>' +'      </td>' +'      <td style="border-left: solid black thick; padding-left: 10px; vertical-align: top;" id="delsort-td">' +'          <select multiple="multiple" data-placeholder="Select a deletion sorting category..."></select>' +'          <button id="add-custom-button" class="mw-ui-button mw-ui-progressive mw-ui-quiet">Add custom</button>' +'      </td>' +'    </tr>' +'  </table>' +'  <button style="position: absolute; top: 5px; right: 5px;" id="close-button" class="mw-ui-button mw-ui-destructive mw-ui-quiet">Close</button>' +'</div>' );            $( "#add-custom-button" ).click( addCustomField );            $( "#close-button" ).click( function () { $( "#delsort" ).remove(); } );            var afdcHtml = "";            Object.keys( afdcCategories ).forEach( function ( code, i ) {                if ( i % 2 === 0 ) afdcHtml += "<tr>";                afdcHtml += "<td><input type='radio' name='afdc' value='" + code + "' id='afdc-" + code + "' /><label for='afdc-" + code + "'>" + afdcCategories[ code ] + "</label></td>";                if ( i % 2 !== 0 ) afdcHtml += "</tr>";            } );            // If there are an odd number of AFDC cats, we need to close off the last row            if ( Object.keys( afdcCategories ).length % 2 !== 0 ) afdcHtml += "</tr>";            $( "#afdc" ).html( afdcHtml );            // Build the deletion sorting categories            delsortCategoriesPromise.done( function ( delsortCategories ) {                $.each( delsortCategories, function ( groupName, categories ) {                    var group = $( "<optgroup>" )                        .appendTo( "#delsort select" )                        .attr( "label", groupName );                    $.each( categories, function ( index, category ) {                        group.append( $( "<option>" )                                      .val( category )                                      .text( category )                                      .addClass( "delsort-category" ) );                    } );                } );                getWikitext( mw.config.get( "wgPageName" ) ).then( function ( wikitext ) {                    autofillAfdc( wikitext );                    // Autofill the delsort box                    var DELSORT_RE = /:<small class="delsort-notice">(.+?)<\/small>/g;                    var DELSORT_LIST_RE = /\[\[Wikipedia:WikiProject Deletion sorting\/(.+?)\|.+?\]\]/;                    var delsortMatch;                    var delsortListMatch;                    do {                        delsortMatch = DELSORT_RE.exec( wikitext );                        if( delsortMatch !== null ) {                            delsortListMatch = DELSORT_LIST_RE.exec( delsortMatch[1] );                            if( delsortListMatch !== null ) {                                currentDelsortCategories.push( delsortListMatch[1] );                                var delsortOption = document.querySelector( "option.delsort-category[value='" + delsortListMatch[1] + "']" );                                if( delsortOption ) {                                    delsortOption.selected = true;                                }                            }                        }                    } while( delsortMatch );                    // Now that we've updated the underlying <select>, ask Chosen to                    // update the visible search box                    $( "#delsort select" ).trigger( "chosen:updated" );                } ); // end getWikitext            } ); // end delsortCategoriesPromise            // Initialize the special chosen.js select box            // (some code stolen from http://stackoverflow.com/a/27445788)            $( "#delsort select" ).chosen();            $( "#delsort .chzn-container" ).css( "text-align", "left" );            // Add the button that triggers sorting            $( "#delsort" ).append( $( "<div>" )                    .css( "text-align", "center" )                    .append( $( "<button> ")                        .addClass( "mw-ui-button" )                        .addClass( "mw-ui-progressive" )                        .attr( "id", "sort-button" )                        .text( "Save changes" )                        .click( function () {                            // Make a status list                            $( "#delsort" ).append( $( "<ul> ")                                                    .attr( "id", "status" ) );                            // Build a list of categories                            var categories = $( "#delsort select" ).val() || [];                            $( ".custom-delsort-field" ).each( function ( index, element ) {                                categories.push( $( element ).val() );                            } );                            categories = categories.filter( Boolean ); // remove empty strings                            categories = removeDups( categories );                            // Only allow categories that aren't already there                            categories = categories.filter( function ( elem ) {                                return currentDelsortCategories.indexOf( elem ) < 0;                            } );                                                        // Obtain the target AFDC category, brought to you by http://stackoverflow.com/a/24886483/1757964                            var afdcTarget = document.querySelector("input[name='afdc']:checked").value;                                                        // Actually do the delsort                            saveChanges( categories, afdcTarget );                        } ) ) );        } );    } // End if ( mw.config.get( "wgPageName" ).indexOf('Wikipedia:Articles_for_deletion/') ... )    /*     * Autofills the AFDC radio button group based on the current     * page's wikitext     */    function autofillAfdc( wikitext ) {        var regexMatch = /REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD(?:\|(.*))?}}/.exec( wikitext );        if ( regexMatch ) {            var templateParameter = regexMatch[1];            if ( templateParameter ) {                currentAfdcCat = templateParameter;                if ( templateParameter.length === 1 ) {                    var currentClass = templateParameter.toLowerCase();                    $( "#afdc-" + currentClass ).prop( "checked", true );                }            }        }    }    /*     * Saves the changes to the current discussion page by adding delsort notices (if applicable) and updating the AFDC cat     */    function saveChanges( cats, afdcTarget ) {        var changingAfdcCat = currentAfdcCat.toLowerCase() !== afdcTarget;        // Indicate to the user that we're doing some deletion sorting        $( "#delsort-table" ).remove();        $( "#delsort #sort-button" )            .text( "Sorting " + ( changingAfdcCat ? "and categorizing " : "" ) + "discussion..." )            .prop( "disabled", true )            .fadeOut( 400, function () {                $( this ).remove();            } );        var categoryTitleComponent = ( cats.length === 1 ) ? ( "the \"" + cats[0] + "\" category" ) : ( cats.length + " categories" );        var afdcTitleComponent = changingAfdcCat ? " and categorizing it as " + afdcCategories[ afdcTarget ] : "";        $( "#delsort-title" )            .html( "Sorting discussion into " + categoryTitleComponent + afdcTitleComponent + "<span id=\"delsort-dots\"></span>" );        // Start the animation, using super-advanced techniques        var animationInterval = setInterval( function () {            $( "#delsort-dots" ).text( $( "#delsort-dots" ).text() + "." );            if( $( "#delsort-dots" ).text().length > 3 ) {                $( "#delsort-dots" ).text( "" );            }        }, 600 );        // Place (a) notification(s) on the discussion and update its AFDC cat        var editDiscussionDeferred = postDelsortNoticesAndUpdateAfdc( cats, afdcTarget );        // List the discussion at the DELSORT pages        var deferreds = cats.map( listAtDelsort );        // We still have to wait for the discussion to be edited        deferreds.push( editDiscussionDeferred );        // When everything's done, say something        $.when.apply( $, deferreds ).then( function () {            // Call the done hook            if( window.delsortDoneHook ) {                window.delsortDoneHook();            }            // We're done!            $( "#delsort-title" )                .text( "Done " + ( changingAfdcCat ? "updating the discussion's AFDC category and " : "" ) + "sorting discussion into " + categoryTitleComponent + "." );            showStatus( "<b>Done!</b> " + ( changingAfdcCat ? "The discussion's AFDC was updated and it was" : "Discussion was" ) + " sorted into " + categoryTitleComponent + ". (" )                .append( $( "<a>" )                         .text( "reload" )                         .attr( "href", "#" )                         .click( function () { document.location.reload( true ); } ) )                .append( ")" );            clearInterval( animationInterval );        } );    }    /*     * Adds a new status to the status list, and returns the newly-displayed element.     */    function showStatus( newStatus ) {        return $( "<li>" )             .appendTo( "#delsort ul#status" )             .html( newStatus );    }    /*     * Adds some notices to the discussion page that this discussion was sorted.     */    function postDelsortNoticesAndUpdateAfdc( cats, afdcTarget ) {        var changingAfdcCat = currentAfdcCat.toLowerCase() !== afdcTarget,            deferred = $.Deferred(),            statusElement = showStatus( "Updating the discussion page..." );        getWikitext( mw.config.get( "wgPageName" ) ).then( function ( wikitext ) {            try {                statusElement.html( "Processing wikitext..." );                // Process wikitext                // First, add delsort notices                wikitext += createDelsortNotices( cats );                // Then, update the AFDC category                var afdcMatch = wikitext.match( /REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD/ );                if ( afdcMatch && afdcMatch[ 0 ] ) {                    var afdcMatchIndex = wikitext.indexOf( afdcMatch[ 0 ] ) + afdcMatch[ 0 ].length,                        charAfterTemplateName = wikitext[ afdcMatchIndex ];                    if ( charAfterTemplateName === "}" ) {                        wikitext = wikitext.slice( 0, afdcMatchIndex ) + "|" + afdcTarget.toUpperCase() + wikitext.slice( afdcMatchIndex );                    } else if ( charAfterTemplateName === "|" ) {                        wikitext = wikitext.replace( "|" + currentAfdcCat + "}}", "|" + afdcTarget.toUpperCase() + "}}" );                    }                }                statusElement.html( "Processed wikitext. Saving..." );                var catPlural = ( cats.length === 1 ) ? "" : "s";                $.ajax( {                    url: mw.util.wikiScript( "api" ),                    type: "POST",                    dataType: "json",                    data: {                        format: "json",                        action: "edit",                        title: mw.config.get( "wgPageName" ),                        summary: "Updating nomination page with notices" + ( changingAfdcCat ? " and new AFDC cat" : "" ) + ADVERTISEMENT,                        token: mw.user.tokens.get( "csrfToken" ),                        text: wikitext                    }                } ).done ( function ( data ) {                    if ( data && data.edit && data.edit.result && data.edit.result == "Success" ) {                        statusElement.html( cats.length + " notice" + catPlural + " placed on the discussion!" );                        if ( changingAfdcCat ) {                            if ( currentAfdcCat ) {                                var formattedCurrentAfdcCat = currentAfdcCat.length === 1 ? afdcCategories[ currentAfdcCat.toLowerCase() ] : currentAfdcCat;                                showStatus( "Discussion's AFDC category was changed from " + formattedCurrentAfdcCat + " to " + afdcCategories[ afdcTarget ] + "." );                            } else {                                showStatus( "Discussion categorized under " + afdcCategories[ afdcTarget ] + " with AFDC." );                            }                        }                        deferred.resolve();                    } else {                        statusElement.html( "While editing the current discussion page, the edit query returned an error. =(" );                        deferred.reject();                    }                } ).fail ( function() {                    statusElement.html( "While editing the current discussion page, the AJAX request failed." );                    deferred.reject();                } );            } catch ( e ) {                statusElement.html( "While getting the current page content, there was an error." );                console.log( "Current page content request error: " + e.message );                deferred.reject();            }        } ).fail( function () {            statusElement.html( "While getting the current content, there was an AJAX error." );            deferred.reject();        } );        return deferred;    }    /*     * Turns a list of delsort categories into a number of delsort template notice substitutions.     */    function createDelsortNotices( cats ) {        if ( Array.isArray(cats) && ! cats.length ) return '';        var appendText = "\n{{subst:Deletion sorting/multi";        cats.forEach( function ( cat ) {            appendText += "|" + cat;        } );        return appendText + "|sig=~~" + "~~}}"; // string concat to prevent it from being transformed into my signature    }        /*     * Adds a listing at the DELSORT page for the category.     */    function listAtDelsort( cat ) {        // Make a status element just for this category        var statusElement = showStatus( "Listing this discussion at DELSORT/" +                                        cat + "..." );        // Clarify our watchlist behavior for this edit        var allowedWatchlistBehaviors = ["watch", "unwatch", "preferences",                "nochange"];        var watchlistBehavior = "nochange"; // no watchlist change by default        if( window.delsortWatchlist && allowedWatchlistBehaviors.indexOf(                window.delsortWatchlist.toLowerCase() ) >= 0 ) {            watchlistBehavior = window.delsortWatchlist.toLowerCase();        }        var listTitle = "Wikipedia:WikiProject Deletion sorting/" + cat;                // First, get the current wikitext for the DELSORT page        return $.getJSON(            mw.util.wikiScript("api"),            {                format: "json",                action: "query",                prop: "revisions",                rvprop: "content",                rvslots: "main",                rvlimit: 1,                titles: listTitle,                redirects: "true",                formatversion: 2,            }        ).then( function ( data ) {            var wikitext = data.query.pages[0].revisions[0].slots.main.content;            var properTitle = data.query.pages[0].title;            try {                statusElement.html( "Got the DELSORT/" + cat + " listing wikitext, processing..." );                                // Actually edit the content to include the new listing                var newDelsortContent = wikitext.replace("directly below this line -->", "directly below this line -->\n\{\{" + mw.config.get("wgPageName") + "\}\}");                                // Then, replace the DELSORT listing with the new content                $.ajax( {                    url: mw.util.wikiScript( "api" ),                    type: "POST",                    dataType: "json",                    data: {                        format: "json",                        action: "edit",                        title: properTitle,                        summary: "Listing [[" + mw.config.get("wgPageName") + "]]" + ADVERTISEMENT,                        token: mw.user.tokens.get( "csrfToken" ),                        text: newDelsortContent,                        watchlist: watchlistBehavior                    }                } ).done ( function ( data ) {                    if ( data && data.edit && data.edit.result && data.edit.result == "Success" ) {                        statusElement.html( "Listed page at <a href=" + mw.util.getUrl( listTitle ) + ">the " + cat + " deletion sorting list</a>!" );                    } else {                        statusElement.html( "While listing at DELSORT/" + cat + ", the edit query returned an error. =(" );                    }                } ).fail ( function() {                    statusElement.html( "While listing at DELSORT/" + cat + ", the ajax request failed." );                } );            } catch ( e ) {                statusElement.html( "While getting the DELSORT/" + cat + " content, there was an error." );                console.log( "DELSORT content request error: " + e.message );                //console.log( "DELSORT content request response: " + JSON.stringify( data ) );            }        } ).fail( function () {            statusElement.html( "While getting the DELSORT/" + cat + " content, there was an AJAX error." );        } );    }    /**     * Gets the wikitext of a page with the given title (namespace required).     */    function getWikitext( title ) {        return $.getJSON(            mw.util.wikiScript("api"),            {                format: "json",                action: "query",                prop: "revisions",                rvprop: "content",                rvslots: "main",                rvlimit: 1,                titles: title,                formatversion: 2,            }        ).then( function ( data ) {            return data.query.pages[0].revisions[0].slots.main.content;        } );    }    /**     * Removes duplicates from an array.     */    function removeDups( arr ) {        var obj = {};        for( var i = 0; i < arr.length; i++ ) {            obj[arr[i]] = 0;        }        return Object.keys( obj );    }}( jQuery, mediaWiki ) );//</nowiki>