AO3 [Wrangling]: Mass-Unwrangleable

Mass-select tags in the Unfilterable or Unwrangleable bin to move into the other bin

Fra og med 26.07.2022. Se den nyeste version.

// ==UserScript==
// @name         AO3 [Wrangling]: Mass-Unwrangleable
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @description  Mass-select tags in the Unfilterable or Unwrangleable bin to move into the other bin
// @author       escctrl
// @version      1.0
// @match        *://*.archiveofourown.org/tags/*/wrangle?*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license      MIT
// ==/UserScript==

(function($) {
    'use strict';

    const binParams = new URLSearchParams(window.location.search);
    const binStatus = binParams.has('status') ? binParams.get('status') : "";

    // don't proceed with the rest of the script if we're not on an unfilterable or unwrangleable bin
    if (binStatus != "unfilterable" && binStatus != "unwrangleable") { return false; }

    // build the additional button to mass-set to unwrangleable
    // in an unwrangleable bin, the label says to deselect the unwrangleable status
    const mass_label = (binStatus == "unwrangleable") ? "Remove Unwrangleable flag" : "Set Unwrangleable flag";
    const mass_button = '<button name="wrangle_unwrangleable" style="padding-left: 0.75em;">' + mass_label + '</button> ';
    $('#wrangulator p.submit.actions').prepend(mass_button);

    // helper vars
    const errorMsg = [];
    var completeTags = 0;
    var selectedTags = [];

    // add the event listener to the button with function to load the Edit pages
    $('button[name="wrangle_unwrangleable"]').click(function(e) {

        // stop button from automatically submitting the form it's in
        e.preventDefault();

        // grab the selected tags
        selectedTags = array($('input[name="selected_tags[]"]')).filter(inp => inp.checked);
        if (selectedTags.length < 1) { alert("Please select at least one tag!"); return false; }

        // now let's run through all the tags and update them
        $(selectedTags).each(function(i, tag) {

            var url = $(tag).closest('tr').find('a[href$="/edit"]')[0]; // nifty jQuery, will work with icon action buttons and without
            url = url.href; // jQuery would return the relative URL, but we need the absolute
            const tagFrame = document.createElement("iframe");

            // create the iframe, hide it, add it to the DOM, and attach an event listener to make make the checkbox changes
            // we're routing through: the iFrame we're operating in, and the index of the tag that's being changed
            $(tagFrame).hide().appendTo('body').one('load', function() { bgEditLoad(tagFrame, i); }); // .one() runs only once and removes itself
            tagFrame.src = url;

        });

    });


    // event listener function to do the actual Edit page changes in the iframe
    function bgEditLoad(tagFrame, iTag) {

        // need to pick up on ao3jail. since the load event always triggers on iFrames, and HTTP responses are impossible to get a hold of, we'll just look at the page content...
        if ($(tagFrame).contents().find('#edit_tag').length != 1) {
            errorMsg[iTag] = "Page could not load. Retry later";
            // manually trigger the event function to do the rest of the work
            bgEditSubmit(tagFrame, iTag);
            return;
        }

        // switch the state of the unwranglable checkbox in the iframe
        const fieldUnwrangleable = $(tagFrame).contents().find('#tag_unwrangleable')[0];
        fieldUnwrangleable.checked = fieldUnwrangleable.checked == true ? false : true;

        // add another event listener to retrieve the fail/success message, and submit the iframe
        $(tagFrame).one('load', function() { bgEditSubmit(tagFrame, iTag); });
        $(tagFrame).contents().find('#edit_tag')[0].submit();
    }

    // event listener function to catch errors occurring in the iframe after submitting the form
    function bgEditSubmit(tagFrame, iTag) {

        // need to pick up on ao3jail. since the load event always triggers on iFrames, and HTTP responses are impossible to get a hold of, we'll just look at the page content...
        if ($(tagFrame).contents().find('#edit_tag').length != 1) {
            errorMsg[iTag] = "Page could not load. Retry later";
        }
        // tracking any errors we've run into
        else {
            const err = $(tagFrame).contents().find('#error');
            if (err.length > 0) { errorMsg[iTag] = err[0].innerHTML; }
        }

        document.body.removeChild(tagFrame);

        // if this was the last tag, we can write out errors on the bin's page
        // note: there are no iframes open anymore
        completeTags++;
        if (completeTags == selectedTags.length) {
            finish();
        }
    }

    // write out all errors that occurred and all tags that were changed successfully
    function finish() {

        const successMsg = [];
        const failMsg = [];

        $(selectedTags).each(function(i, tag) {
            var tagRow = $(tag).closest('tr');
            var tagURL = $(tagRow).find('a[href$="/edit"]')[0];
                tagURL = tagURL.href;
            var tagName = $(tagRow).find('th label')[0];
                tagName = tagName.innerText;

            // errorMsg[] has the corresponding indices so we know which tag failed
            if (errorMsg[i] != undefined) { failMsg.push('<a href="' + tagURL + '">' + tagName + '</a>: ' + errorMsg[i]); }
            else {
                successMsg.push('<a href="' + tagURL + '">' + tagName + '</a>');
                // remove the changed tags from the bin page
                $(tagRow).remove();
            }
        });

        // constructing the response message to the user and showing it
        if (failMsg.length > 0) {
            const fail = '<div id="error" class="error">The following tags\' Unwrangleable flag could not be updated:<br />' + failMsg.join('<br />') + '</div>';
            $(fail).prependTo('#main');
            window.scrollTo(0,0);
        }
        if (successMsg.length > 0) {
            const success = 'The following tags\' Unwrangleable flag was updated successfully:<br />' + successMsg.join(', ');
            $('.flash').clone().prependTo('#main').addClass('notice').html(success);
            window.scrollTo(0,0);
        }
    }

    // the return value of document.querySelectorAll is technically a "NodeList", which can be indexed like an array, but
    // doesn't have helpful functions like .map() or .forEach(). So this is a simple helper function to turn a NodeList
    // (or any other array-like object (indexed by integers starting at zero)) into an array
    const array = a => Array.prototype.slice.call(a, 0);


})(jQuery);