Greasy Fork is available in English.

AO3: [Wrangling] Smaller Tag Search

makes the new Tag Search form take up less space (best on desktop/landscape screens)

// ==UserScript==
// @name         AO3: [Wrangling] Smaller Tag Search
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @version      3.1
// @description  makes the new Tag Search form take up less space (best on desktop/landscape screens)
// @author       escctrl
// @match        *://*.archiveofourown.org/tags/search*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
// @grant        none
// @license      MIT
// ==/UserScript==


(function($) {
    'use strict';

    // on retry later, break off the rest of this script to avoid console errors
    if ($('#new_tag_search').length == 0) return;

    /*********************************************************
     GUI CONFIGURATION
     *********************************************************/

    // ASC/DESC translation for the different sort options
    // x -> [ASC, DESC]
    var dir_alias = new Map([["name", ["A → Z", "Z → A"]], ["created_at", ["oldest → newest", "newest → oldest"]], ["uses", ["fewest → most", "most → fewest"]]]);

    // standard config in case nothing was configured so far
    var hide_sort_options = false;
    var hide_labels = false;
    var make_textinput_smaller = false;
    var default_search_type = "";
    var default_search_status = "";
    var default_sort_by = "name";
    var default_sort_dir = "asc";

    // load storage on page startup
    var stored_config = new Map(JSON.parse(localStorage.getItem('smallertagsearch')));
    if (stored_config.size > 0) {
        hide_sort_options = stored_config.get('sorthide') == "true" ? true : false;
        hide_labels = stored_config.get('selectlabels') == "true" ? true : false;
        make_textinput_smaller = stored_config.get('textlabels') == "true" ? true : false;
        default_search_type = stored_config.get('deftype');
        default_search_status = stored_config.get('defstatus');
        default_sort_by = stored_config.get('defsortby');
        default_sort_dir = stored_config.get('defsortdir');
    }

    // if the background is dark, use the dark UI theme to match
    let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "ui-darkness" : "base";

    // the config dialog container
    let cfg = document.createElement('div');
    cfg.id = 'tagSearchDialog';

    // adding the jQuery stylesheet to style the dialog, and fixing the interferance of AO3's styling
    $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
    .prepend(`<script src="https://use.fontawesome.com/ed555db3cc.js" />`)
    .append(`<style tyle="text/css">#${cfg.id}, .ui-dialog .ui-dialog-buttonpane button {font-size: revert; line-height: 1.286;}
    #${cfg.id} form {box-shadow: revert; cursor:auto;}
    #${cfg.id} legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
    #${cfg.id} #tagsearchdefault label { display: inline-block; width: 8em; }
    #${cfg.id} #tagsearchdisplay label { width: 16em; text-align: left; }
    #${cfg.id} fieldset {background: revert; box-shadow: revert;}
    </style>`);

    var selected = 'selected="selected"';
    var checked = 'checked="checked"';
    var default_sort = default_sort_by + "-" + default_sort_dir;

    // helper function to create a dropdown for tag types with the correct preselected option
    function buildTypeOptions(compare) {
        let options = "";
        $(["", "Fandom", "Character", "Relationship", "Freeform", "UnsortedTag"]).each( function() {
            options += `<option value="${this}" ${(compare == this) ? selected : ""}>${
                (this == "") ? "Any tag type" :
                (this == "UnsortedTag") ? "Unsorted Tags" : this
            }</option>`;
        });
        return options;
    }

    $(cfg).html(`<form>
    <fieldset id='tagsearchdisplay'>
        <legend>Display</legend>
        <p>The script already turns radiobuttons into selection lists. Choose what else to hide to make the tag search form smaller.</p>
        <label for="selectlabels">Hide Labels on Selection Lists</label><input type="checkbox" name="selectlabels" id="selectlabels" ${(hide_labels) ? checked : ""}><br />
        <label for="sorthide">Hide Sort Options</label><input type="checkbox" name="sorthide" id="sorthide" ${(hide_sort_options) ? checked : ""}><br />
        <label for="textlabels">Hide Labels on Textfields</label><input type="checkbox" name="textlabels" id="textlabels" ${(make_textinput_smaller) ? checked : ""}>
    </fieldset>
    <fieldset id='tagsearchdefault'>
        <legend>Default Selections</legend>
        <p>Pick defaults for the tag type, wrangling status, and sort order.</p>
        <label for="deftype">Tag Type</label>
        <select name="deftype" id="deftype">
          ${buildTypeOptions(default_search_type)}
        </select><br />
        <label for="defstatus">Wrangling Status</label>
        <select name="defstatus" id="defstatus">
          <option value="" ${(default_search_status == "") ? selected : ""}>Any wrangling status</option>
          <option value="Canonical" ${(default_search_status == "Canonical") ? selected : ""}>Canonical</option>
          <option value="Non-canonical" ${(default_search_status == "Non-canonical") ? selected : ""}>Non-canonical</option>
        </select>
        <label for="defsort">Sort By</label>
        <select name="defsort" id="defsort">
          <option value="name-asc" ${default_sort == "name-asc" ? selected : ""}>Name, ${dir_alias.get('name')[0]}</option>
          <option value="name-desc" ${default_sort == "name-desc" ? selected : ""}>Name, ${dir_alias.get('name')[1]}</option>
          <option value="created_at-asc" ${default_sort == "created_at-asc" ? selected : ""}>Creation Date, ${dir_alias.get('created_at')[0]}</option>
          <option value="created_at-desc" ${default_sort == "created_at-desc" ? selected : ""}>Creation Date, ${dir_alias.get('created_at')[1]}</option>
          <option value="uses-asc" ${default_sort == "uses-asc" ? selected : ""}>Uses, ${dir_alias.get('uses')[0]}</option>
          <option value="uses-desc" ${default_sort == "uses-desc" ? selected : ""}>Uses, ${dir_alias.get('uses')[1]}</option>
        </select>
    </fieldset>
    </form>`);

    // attach it to the DOM so that selections work (but only if #main exists, else it might be a Retry Later error page)
    if ($("#main").length == 1) $("body").append(cfg);

    // optimizing the size of the GUI in case it's a mobile device
    let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
    dialogwidth = dialogwidth > 500 ? 500 : dialogwidth * 0.9;

    // initialize the dialog (but don't open it)
    $( "#tagSearchDialog" ).dialog({
        appendTo: "#main",
        modal: true,
        title: 'Smaller Tag Search Config',
        draggable: true,
        resizable: false,
        autoOpen: false,
        width: dialogwidth,
        position: {my:"center", at: "center top"},
        buttons: {
            Reset: deleteConfig,
            Save: storeConfig,
            Cancel: function() { $( "#tagSearchDialog" ).dialog( "close" ); }
        }
    });

    // event triggers if form is submitted with the <enter> key
    $( "#tagSearchDialog form" ).on("submit", (e) => {
        e.preventDefault();
        storeConfig();
    });

    function deleteConfig() {
        // deselects all buttons, empties all fields in the form
        $('#tagSearchDialog form').trigger("reset");

        // deletes the localStorage
        localStorage.removeItem('smallertagsearch');

        $( "#tagSearchDialog" ).dialog( "close" );
        location.reload();
    }

    function storeConfig() {
        // build a Map() for enabled standard buttons => button -> true/false
        let storestd = new Map();
        $( "#tagSearchDialog input[name]" ).each(function() { storestd.set( $(this).prop('name'), String($(this).prop('checked')) ); });
        $( "#tagSearchDialog select[name]" ).each(function() {
            if ($(this).prop('name') == "defsort") {
                let val = $(this).prop('value').split('-');
                storestd.set( 'defsortby', val[0] );
                storestd.set( 'defsortdir', val[1] );
            }
            else
                storestd.set( $(this).prop('name'), String($(this).prop('value')) );
        });
        localStorage.setItem('smallertagsearch', JSON.stringify(Array.from(storestd.entries())));

        $( "#tagSearchDialog" ).dialog( "close" );
        location.reload();
    }

    /* CREATING THE LINK TO OPEN THE CONFIGURATION DIALOG */

    // if no other script has created it yet, write out a "Userscripts" option to the main navigation
    if ($('#scriptconfig').length == 0) {
        $('#header ul.primary.navigation li.dropdown').last()
            .after(`<li class="dropdown" id="scriptconfig">
                <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
                <ul class="menu dropdown-menu"></ul></li>`);
    }
    // then add this script's config option to navigation dropdown
    $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_tagsearch">Smaller Tag Search</a></li>`);

    // on click, open the configuration dialog
    $("#opencfg_tagsearch").on("click", function(e) {
        $( "#tagSearchDialog" ).dialog('open');

        // turn checkboxes and radiobuttons into pretty buttons (must be after 'open' for dropdowns to work)
        $( "#tagSearchDialog input[type='checkbox'], #tagSearchDialog input[type='radio']" ).checkboxradio();
        $( "#tagSearchDialog select" ).selectmenu();
    });


    /*********************************************************
     CHANGING THE DISPLAY ON THE PAGE
     *********************************************************/

    // ** 1 ** Display the Type and Wrangling Status choices as dropdowns again to save screen space

    // switch default_search_status config to values AO3 recognizes
    switch (default_search_status) {
        case "Canonical":
            default_search_status = "T";
            break;
        case "Non-canonical":
            default_search_status = "F";
            break;
        default:
            default_search_status = "";
            break;
    }

    // check URL parameters so we can set the correct selected="selected" option from the last search
    var searchParams = new URLSearchParams(window.location.search);
    var search_type = searchParams.has('tag_search[type]') ? searchParams.get('tag_search[type]') : default_search_type;
    var search_canonical = searchParams.has('tag_search[canonical]') ? searchParams.get('tag_search[canonical]') : default_search_status;
    var search_sortby = searchParams.has('tag_search[sort_column]') ? searchParams.get('tag_search[sort_column]') : default_sort_by;
    var search_sortdir = searchParams.has('tag_search[sort_direction]') ? searchParams.get('tag_search[sort_direction]') : default_sort_dir;

    // choose the default sorting
    $("#tag_search_sort_column").val(search_sortby);
    $("#tag_search_sort_direction").val(search_sortdir);

    // create new dropdown for tag types (and add descriptor on "Any" option if labels are hidden)
    var type_select = `<select id="tag_search_type" name="tag_search[type]">${buildTypeOptions(search_type)}</select>`;

    // create new dropdown for tag wrangling status (and add descriptor on "Any" option if labels are hidden)
    var status_select = '<select id="tag_search_status" name="tag_search[canonical]">'
    + '<option value="T"' + (search_canonical == "T" ? selected : "") + '>Canonical</option>'
    + '<option value="F"' + (search_canonical == "F" ? selected : "") + '>Non-canonical</option>'
    + '<option value=""' + (search_canonical == "" ? selected : "") + '>Any wrangling status</option>'
    + '</select>';

    // add in new dropdowns
    var searchform = $("#new_tag_search dl dd fieldset");
    $(searchform).first().before(type_select);
    $(searchform).last().before(status_select);

    // wrap tag type and status labels in a <label> element, so they behave the same as the sorting labels
    $(searchform).first().parent().prev().wrapInner('<label for="tag_search_type"></label>');
    $(searchform).last().parent().prev().wrapInner('<label for="tag_search_status"></label>');

    // remove the radiobuttons at last
    $(searchform).remove();

    // ** 2 ** hide the description below the fandom field
    $("#fandom-field-description").hide();

    // ** 3 ** Hide sort options - or make the ASC/DESC make more sense
    if (hide_sort_options) {
        $("#tag_search_sort_column").parent().hide().prev().hide();
        $("#tag_search_sort_direction").parent().hide().prev().hide();
    }
    else {
        // on page load: align the asc/desc text with what we're sorting by
        $("#tag_search_sort_direction [value='asc']").text(dir_alias.get($("#tag_search_sort_column").val())[0]);
        $("#tag_search_sort_direction [value='desc']").text(dir_alias.get($("#tag_search_sort_column").val())[1]);
        // now the same for when a different sort is chosen
        $("#tag_search_sort_column").on('change', function(e) {
            $("#tag_search_sort_direction [value='asc']").text(dir_alias.get($(e.target).val())[0]);
            $("#tag_search_sort_direction [value='desc']").text(dir_alias.get($(e.target).val())[1]);
            if (hide_labels) $("#tag_search_sort_direction option").prepend("List ");
        });
    }

    // ** 4 ** Reduce label widths and hide those of the dropdowns
    $("#tag_search_status").parent().prev().find('label').html('Status');

    var style = $('<style type="text/css"></style>').appendTo($('head'));
    style.html(`.tagsearch-label { width: 12%; min-width: unset; } .tagsearch-select { width: 20%; margin-left: 0.2em; margin-right: 0.2em; }
                #new_tag_search p.submit.actions { margin-top: -4em; } /* moves Search button up into the dropdown line */
                .tagsearch-select select { min-width: unset; } /* this avoids labels overlapping on zoom */
                .tagsearch-floatlabel { width: 15%; clear: none; margin-left: 2%; } .tagsearch-input { width: 45%; margin-right: 2em; }  `);

    // add classes to all the dds and dts
    $("#new_tag_search dl dt").addClass("tagsearch-label");
    $("#new_tag_search dl dd select").parent().addClass("tagsearch-select");

    // no labels: all shown dropdowns move into a single line
    var search_labels = $(".tagsearch-label");
    if (hide_labels) {
        $(search_labels).slice(2).hide();
        // while we're here, add descriptors to the sort options (if shown)
        if (!hide_sort_options) {
            $("#tag_search_sort_column option").prepend("Sort by ");
            $("#tag_search_sort_direction option").prepend("List ");
        }
    }
    // with labels: float the even-numbered labels to build two columns of dropdowns
    else if (hide_sort_options) {
            $(search_labels).slice(3,4).addClass("tagsearch-floatlabel");
            $(search_labels).slice(5,6).addClass("tagsearch-floatlabel");
    }
    // when all four dropdowns are shown, change the order so the tag type and status are on top of each other
    else {
        var selects = $('#new_tag_search dl').children();
        var moving = selects.slice(-6,-4);
        $(selects).slice(-6, -4).remove();
        $(selects).slice(-2, -1).before(moving);
        $(selects).slice(-4,-3).addClass("tagsearch-floatlabel");
        $(selects).slice(-2,-1).addClass("tagsearch-floatlabel");
    }

    // ** 5 ** Go extreme - also make the text fields smaller
    if (make_textinput_smaller) {

        // hide the labels
        $(search_labels).slice(0,2).hide();

        // add placeholder text to recognize the fields instead of the labels
        $("#tag_search_name").attr("placeholder", "Enter search term");
        $("#tag_search_fandoms_autocomplete").attr("placeholder", "Choose a fandom");

        // make textfields narrower to fit into a single line
        $("#tag_search_name").parent().addClass("tagsearch-input");
        var searchfandom = $("#tag_search_fandoms_autocomplete").parentsUntil("dd").parent().slice(0,1);
        $(searchfandom).addClass("tagsearch-input");
        // fandom search elements need to lose some margin to make it line up with the tag name textfield
        $(searchfandom).find("li.input").css("margin", "0");
    }
    
    // ** 6 ** jump the focus to the text field if there was no search yet
    if (searchParams == "") {
        document.getElementById('tag_search_name').focus();
    }

})(jQuery);

// helper function to determine whether a color (the background in use) is light or dark
// https://awik.io/determine-color-bright-dark-using-javascript/
function lightOrDark(color) {
    var r, g, b, hsp;
    if (color.match(/^rgb/)) { color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
        r = color[1]; g = color[2]; b = color[3]; }
    else { color = +("0x" + color.slice(1).replace(color.length < 5 && /./g, '$&$&'));
        r = color >> 16; g = color >> 8 & 255; b = color & 255; }
    hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
    if (hsp>127.5) { return 'light'; } else { return 'dark'; }
}