AO3: [Wrangling] Boot tags to another fandom!

Easily move tags from one fandom to another via wrangulator!!

// ==UserScript==
// @name         AO3: [Wrangling] Boot tags to another fandom!
// @description  Easily move tags from one fandom to another via wrangulator!!
// @version      1.0.1

// @author       owlwinter
// @namespace    N/A
// @license      MIT license

// @match        *://*.archiveofourown.org/tags/*/wrangle?*status=unfilterable*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    //Gets the edit url of the tag from the checkbox
    const get_url = function get_url(checkbox) {
        //If iconify is enabled, this will return the tag's link
        const a = checkbox.parentElement.parentElement.querySelector("ul.actions > li[title='Edit'] > a");
        if (a) {
            return a.href;
        }
        //If iconify is not enabled, we'll use the default path
        const buttons = checkbox.parentElement.parentElement.querySelectorAll("ul.actions > li > a");
        return array(buttons).filter(b => b.innerText == "Edit")[0].href;
    }

    //Hides the checked rows after we do our work
    const delete_tag_row = function delete_tag_row(checkbox) {
        const row = checkbox.parentElement.parentElement;
        row.parentElement.removeChild(row);
    }

    //Each time we finish a request, we'll add 1 to the "done" count
    //When it's equal to the number of tags we had to make requests for, we know we gone through all the checked rows!
    var done;
    //Holds any errors from form submit
    var errors;
    //All tags selected to boot
    var tags;

    // Called for each checked tag
    // Submits form data & passes on any resulting error messages
    // If succesfully done for every selected tag, will also display success banner :)
    // `url` is the URL that will process the form submission
    // `fullfandoms` is an array of all the new fandoms to be added
    // `tag` is the tag being edited
    // `fd` is the new formdata
    const submit_xhr = function submit_xhr(url, fullfandoms, tag, fd) {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function xhr_onreadystatechange() {
            if (xhr.readyState == xhr.DONE ) {
                //After form submitted:
                if (xhr.status == 200) {
                    // Check for errors
                    const err = xhr.responseXML.documentElement.querySelector("#error");
                    if (err) {
                        alert(err.innerText);
                        errors.push(err)
                    }
                    done += 1;
                    //If succesfully done for every selected tag, we want to display a success banner!
                    if (done == tags.length) {
                        // for some reason this seems to always be present on the page, even if there is no content in it
                        var flash = document.getElementsByClassName("flash")[0]
                        flash.innerHTML = "";
                        flash.classList.add("notice")
                        if (errors.length != 0) {
                            //In case the user wasn't bonked enough for their error,
                            //puts the red box with the error message from the iframe into the flash action message area
                            errors.forEach(e => {
                                e.parentElement.removeChild(e)
                                flash.appendChild(e);
                            });
                        } else {
                            // happy path :)
                            flash.appendChild(document.createTextNode("The following tags were successfully booted to "));
                            flash.appendChild(document.createTextNode(fullfandoms.toString().replaceAll(',', ', ')))
                            flash.appendChild(document.createTextNode(": "));
                            const as = tags.map(checkbox => {
                                const a = document.createElement("a")
                                a.href = get_url(checkbox);
                                a.target = "_blank"
                                a.innerText = checkbox.labels[0].innerText;
                                return a;
                            });
                            as.forEach((a, i) => {
                                if (i != 0) {
                                    flash.appendChild(document.createTextNode(", "))
                                }
                                flash.appendChild(a);
                            });
                            tags.forEach(checkbox => delete_tag_row(checkbox));
                        }
                        flash.appendChild(document.createElement("br"))
                        flash.scrollIntoView();
                        //Unset explicit background to let it go back to the default CSS background
                        button.style.background = ""
                        button.disabled = false
                        button.innerText = "Boot";
                    }
                }
                else if (xhr.status == 429) {
                    // ~ao3jail
                    alert("Rate limited. Sorry :(")
                } else {
                    alert("Unexpected error, check the console :(")
                    console.log(xhr)
                }
            }
        }
        xhr.open("POST", url)
        xhr.responseType = "document"
        xhr.send(fd)
    }

    // Called for each checked tag
    // Gets the edit form data about the tag and sets up request that will actually submit
    // `url` is the url of the edit tag page
    // `newfandoms` is an array of all the new fandoms to be added
    const boot_xhr = function boot_xhr(url, newfandoms) {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function xhr_onreadystatechange() {
            if (xhr.readyState == xhr.DONE ) {
                if (xhr.status == 200) {
                    //Fandoms the tag has pre-boot
                    const oldfandoms = []
                    const inner_input = xhr.responseXML.documentElement.querySelector("input[name='tag[syn_string]']")
                    const tag = inner_input.value;

                    const form = xhr.responseXML.documentElement.querySelector("#edit_tag")

                    //Removing all fandoms the tag has pre-boot
                    const checks = array(xhr.responseXML.documentElement.querySelectorAll("label > input[name='tag[associations_to_remove][]']")).filter(foo => foo.id.indexOf("parent_Fandom_associations_to_remove") != -1);
                    for (const check of checks) {
                        const name = check.parentElement.nextElementSibling.innerText
                        oldfandoms.push(name)
                        //Edge case - if user boots a tag to a fandom currently on the tag
                        if (!newfandoms.includes(name)) {
                            check.checked = true;
                        }
                    }
                    //Setting the new fandoms on the tag
                    xhr.responseXML.documentElement.querySelector("#tag_fandom_string").value = newfandoms;

                    console.log("Tag at url " + url + " booted from " + oldfandoms.toString() + " and booted to " + newfandoms)

                    const fd = new FormData(form);

                    //Second xhr call that will submit our edited data to the form
                    submit_xhr(form.action, newfandoms, tag, fd);
                } else if (xhr.status == 429) {
                    // ~ao3jail
                    alert("Rate limited. Sorry :(")
                } else {
                    alert("Unexpected error, check the console :(")
                    console.log(xhr)
                }
            }
        }
        xhr.open("GET", url)
        xhr.responseType = "document"
        xhr.send()
    }

    //Runs when the boot button is clicked
    const boot_fandoms = function boot_fandoms(e) {
        e.preventDefault()

        //Gets all the rows with selected tags
        tags = array(document.querySelectorAll("input[name='selected_tags[]']")).filter(inp => inp.checked);

        //If no tags are selected, bonk user >:(
        if (tags.length == 0) {
            alert("You need to select at least one tag to boot!")
            return;
        }

        //The fandoms checked via the Fandom Assignment Shortcut script, if it is installed
        const fandomschecked = array(document.querySelectorAll("input[name='fandom_shortcut']:checked")).map(f => f.value)
        //The fandoms added via wrangulator textbox
        const fandomstext = document.getElementById("fandom_string").value.split(',')
        //Combine both fandom lists, then remove empty elements
        const fullfandoms = fandomschecked.concat(fandomstext).filter(n => n)

        //If no fandoms are selected, bonk user >:(
        if (fullfandoms.length == 0) {
            alert("You need to select at least one fandom to boot to!")
            return;
        }

        //Each time we finish a request, we'll add 1 to "done", and when it's equal to the number of tags we had to make requests for, we know we have finished them all
        done = 0;
        errors = []

        button.disabled = "true";
        button.innerText = "Processing..."
        // There is no special style in the default ao3 styles for disabled buttons, so even when disabled, the button still looks clickable. This sets the background to
        // "lightgrey" instead of the default light -> dark gradient, to make it look more disabled. We undo this later by setting this property back to an empty string
        // after all our requests are done.
        button.style.background = "lightgrey";

        //Runs hard boot xhr call on each tag selected
        tags.forEach(checkbox => {
            const url = get_url(checkbox)
            boot_xhr(url, fullfandoms)
        })
    }

    // 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)

    //Creates boot button
    const button = document.createElement("button");
    button.type = "text";
    button.style.textAlign = "center"
    button.style.marginRight = "5px"
    button.textContent = "Boot";
    button.addEventListener("click", boot_fandoms)
    document.querySelector("dd.submit").prepend(button);

    // Your code here...
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址