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...
- })();