User:Enterprisey/delsort.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
//<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>