Greasy Fork is available in English.

AO3: [Wrangling] Smaller Tag Search

makes the 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      4.1
// @description  makes the 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
// @require      https://update.greasyfork.org/scripts/491888/1355841/Light%20or%20Dark.js
// @grant        none
// @license      MIT
// ==/UserScript==

/* eslint-disable no-multi-spaces */
/* global jQuery, lightOrDark */

(function($) {
    'use strict';

    // --- THE USUAL INIT STUFF AT THE BEGINNING -------------------------------------------------------------------------------

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

    $("head").append(`<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">`);

    let cfg = 'smallertagsearch'; // name of dialog and localstorage used throughout
    let dlg = '#'+cfg;

    /* *** EXAMPLE STORAGE: equivalent of a Map() with settings for reducing size and default selections on pageload
        settings = [ ["text","1"],["labels", "n"],["tag","b"],["sort","b"],["btntxt", "y"],
                     ["deftype","Freeform"],["defstatus",""],["defsortby","name"],["defsortdir","asc"]
                   ];

        text: (1) next to each other, (2) below each other with labels
        tag & sort labels (above options): (y) yes, (n) no
        tag show as: (b) buttons, (s) select
        sort show as: (b) buttons, (s) select, (h) hide
        tag & sort buttontext (on options): (y) yes, (n) no
    */
    let settings = loadConfig();

    // ASC/DESC translation for the different sort options: x -> [ASC, icon for ASC, DESC, icon for DESC]
    var dir_alias = new Map([["name",       ["A → Z",           `<i class="fa fa-sort-alpha-asc"></i>`,   "Z → A",           `<i class="fa fa-sort-alpha-desc"></i>`]],
                             ["created_at", ["oldest → newest", `<i class="fa fa-sort-amount-asc"></i>`,  "newest → oldest", `<i class="fa fa-sort-amount-desc"></i>`]],
                             ["uses",       ["fewest → most",   `<i class="fa fa-sort-numeric-asc"></i>`, "most → fewest",   `<i class="fa fa-sort-numeric-desc"></i>`]]]);
    let getAscDescAlias = (by, dir) => dir_alias.get(by)[ (dir == "asc" ? 0 : 2) ]; // function to retrieve readable name based on which sort-by is selected
    let getAscDescIcon  = (by, dir) => dir_alias.get(by)[ (dir == "asc" ? 1 : 3) ]; // function to retrieve matching icon based on which sort-by is selected

    // figure out which option currently needs to be selected, based on search parameters in the URL vs. configured defaults
    var opt_selected = new Map();
    let params = new URLSearchParams(document.location.search);
    ["tag_search[type]", "tag_search[canonical]", "tag_search[sort_column]", "tag_search[sort_direction]"].forEach((name) => {
        // if we've already done a search, select that option again
        if (params.size !== 0) {
            if (params.get(name)) opt_selected.set(name, params.get(name)); // if this parameter was actually part of the URL
            else { // otherwise go with what AO3 selects as defaults on partial search strings
                switch (name) {
                    case "tag_search[type]":
                    case "tag_search[canonical]":
                        opt_selected.set(name, "");
                        break;
                    case "tag_search[sort_column]": opt_selected.set(name, "name"); break;
                    case "tag_search[sort_direction]": opt_selected.set(name, "asc"); break;
                    default: break;
                }
            }
        }
        // otherwise pick the configured defaults
        else {
            switch (name) {
                case "tag_search[type]": opt_selected.set(name, settings.get("deftype")); break;
                case "tag_search[canonical]":
                    settings.get("defstatus") === "Canonical" ? opt_selected.set(name, "T") :
                    settings.get("defstatus") === "Non-canonical" ? opt_selected.set(name, "F") : opt_selected.set(name, "");
                    break;
                case "tag_search[sort_column]": opt_selected.set(name, settings.get("defsortby")); break;
                case "tag_search[sort_direction]": opt_selected.set(name, settings.get("defsortdir")); break;
                default: break;
            }
        }
    });

    // --- CONFIGURATION DIALOG HANDLING -------------------------------------------------------------------------------

    createDialog();

    function createDialog() {

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

        // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
        $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
        .append(`<style type="text/css">${dlg}, .ui-dialog .ui-dialog-buttonpane button {font-size: revert; line-height: 1.286;}
        ${dlg} form {box-shadow: revert; cursor:auto;}
        ${dlg} fieldset {background: revert; box-shadow: revert; margin-left: 0; margin-right: 0;}
        ${dlg} legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
        ${dlg} fieldset p { padding-left: 0; padding-right: 0; }
        ${dlg} select { width: auto; }
        ${dlg} #tagsearchdefaults label { width: 9em; display: inline-block; }
        </style>`);

        // wrapper div for the dialog
        $("#main").append(`<div id="${cfg}"></div>`);

        let selected = 'selected="selected"';
        let checked = 'checked="checked"';

        let deftype = "", defstatus = "", defsort = "";
        ["Fandom", "Character", "Relationship", "Freeform", ""].forEach(
            v => { deftype += `<option value="${v}" ${v === settings.get("deftype") ? selected : ""}>${v === "" ? 'Any Type' : v}</option>`; }
        );
        ["Canonical", "Non-canonical", ""].forEach(
            v => { defstatus += `<option value="${v}" ${v === settings.get("defstatus") ? selected : ""}>${v === "" ? 'Any Type' : v}</option>`; }
        );
        [["name", "Name"], ["created_at", "Creation Date"], ["uses", "Uses"]].forEach(
            v => { defsort += `<option value="${v[0]}-asc" ${v[0] === settings.get("defsortby") && settings.get("defsortdir") === "asc" ? selected : ""}>${v[1]}, ${getAscDescAlias(v[0], 'asc')}</option>
                               <option value="${v[0]}-desc" ${v[0] === settings.get("defsortby") && settings.get("defsortdir") === "desc" ? selected : ""}>${v[1]}, ${getAscDescAlias(v[0], 'desc')}</option>`; }
        );

        $(dlg).html(`<form>
            <fieldset id='tagsearchdisplay'><legend>Display</legend>
                <p><label for="tagsearchdisplay_text">Show Search Text and Fandom input fields</label><br />
                    <select name="tagsearchdisplay_text" id="tagsearchdisplay_text" style="width: 20em;">
                        <option value="1" ${settings.get('text')==="1" ? selected : ""}>next to each other, without labels</option>
                        <option value="2" ${settings.get('text')==="2" ? selected : ""}>below each other, with labels</option>
                    </select>
                </p>
                <p class="radiocontrol">Show the Tag Type and Status options as<br />
                    <label for="tag_buttons"><i class="fa fa-toggle-on" aria-hidden="true"></i> Buttons</label><input type="radio" name="tagsearchdisplay_tag" id="tag_buttons" value="b" ${settings.get('tag') === "b" ? checked : ""}>
                    <label for="tag_select"><i class="fa fa-caret-square-o-down" aria-hidden="true"></i> Dropdown</label><input type="radio" name="tagsearchdisplay_tag" id="tag_select" value="s" ${settings.get('tag') === "s" ? checked : ""}>
                </p>
                <p class="radiocontrol">Show the Sort By and Direction options as<br />
                    <label for="sort_buttons"><i class="fa fa-toggle-on" aria-hidden="true"></i> Buttons</label><input type="radio" name="tagsearchdisplay_sort" id="sort_buttons" value="b" ${settings.get('sort') === "b" ? checked : ""}>
                    <label for="sort_select"><i class="fa fa-caret-square-o-down" aria-hidden="true"></i> Dropdown</label><input type="radio" name="tagsearchdisplay_sort" id="sort_select" value="s" ${settings.get('sort') === "s" ? checked : ""}>
                    <label for="sort_hide"><i class="fa fa-eye-slash" aria-hidden="true"></i> Hidden</label><input type="radio" name="tagsearchdisplay_sort" id="sort_hide" value="h" ${settings.get('sort') === "h" ? checked : ""}>
                </p>
                <p><label for="tagsearchdisplay_labels">Show labels above Tag Type/Status and Sort By/Direction</label>
                    <input type="checkbox" name="tagsearchdisplay_labels" id="tagsearchdisplay_labels" ${settings.get('labels') === "y" ? checked : ""}>
                    <label for="tagsearchdisplay_btntxt">Show Tag Type/Status and Sort By/Direction buttons with text</label>
                    <input type="checkbox" name="tagsearchdisplay_btntxt" id="tagsearchdisplay_btntxt" ${settings.get('btntxt') === "y" ? checked : ""}>
                </p>
            </fieldset>
            <fieldset id='tagsearchdefaults'>
                <legend>Defaults</legend>
                <p>Pick defaults for the tag type, wrangling status, and sort order.</p>
                <label for="deftype">Tag Type</label>
                <select name="tagsearchdefault_type" id="deftype">
                  ${ deftype }
                </select><br />
                <label for="defstatus">Wrangling Status</label>
                <select name="tagsearchdefault_status" id="defstatus">
                  ${ defstatus }
                </select><br />
                <label for="defsort">Sort By</label>
                <select name="tagsearchdefault_sort" id="defsort" style="width: 20em;">
                  ${ defsort }
                </select>
            </fieldset>
            <!--<fieldset id='tagsearchquick'>
                <legend>Quick Search Buttons</legend>
            </fieldset>-->
        </form>`);

        // 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)
        $( dlg ).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() { $( dlg ).dialog( "close" ); }
            }
        });

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

        // 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_${cfg}">Smaller Tag Search</a></li>`);

        // on click, open the configuration dialog
        $("#opencfg_"+cfg).on("click", function(e) {
            $( dlg ).dialog('open');

            // turn checkboxes and radiobuttons into pretty buttons. only once the dialog is open bc sizing doesn't work correctly on hidden elements
            $( `${dlg} input[type='checkbox']` ).checkboxradio();
            $( `${dlg} select` ).selectmenu({ width: null });
            $( `${dlg} input[type='radio']` ).checkboxradio({ icon: false });
            $( `${dlg} .radiocontrol` ).controlgroup();

        });
    }

    // --- DELEGATED EVENT HANDLERS FOR REACTIVE GUI -------------------------------------------------------------------------------

    // --- HELPER FUNCTIONS TO CREATE GUI HTML -------------------------------------------------------------------------------

    // --- LOCALSTORAGE MANIPULATION -------------------------------------------------------------------------------

    function loadConfig() {
        // load storage on page startup, or default values if there's no storage item
        return new Map(JSON.parse(localStorage.getItem(cfg) || `[["text","1"],["labels","n"],["tag","b"],["sort","s"],["btntxt","n"],["deftype",""],["defstatus",""],["defsortby","name"],["defsortdir","asc"]]`));
    }

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

        // deletes the localStorage
        localStorage.removeItem(cfg);

        $( dlg ).dialog( "close" );
        location.reload();
    }

    function storeConfig() {
        // fill a Map() with the choices in the Config GUI
        let toStore = new Map();

        // checkboxes: show labels?
        $(`${dlg} input[type='checkbox']`).each( function() {
            if ($(this).prop('name') == "tagsearchdisplay_labels") {
                if ($(this).prop('checked')) toStore.set('labels', "y");
                else toStore.set('labels', "n");
            }
            else if ($(this).prop('name') == "tagsearchdisplay_btntxt") {
                if ($(this).prop('checked')) toStore.set('btntxt', "y");
                else toStore.set('btntxt', "n");
            }
        } );
        // radiobuttions: how to show tag type/status and sort by/direction
        $(`${dlg} input[type='radio']:checked`).each( function() {
            if      ($(this).prop('name') == "tagsearchdisplay_tag")  toStore.set('tag', $(this).prop('value'));
            else if ($(this).prop('name') == "tagsearchdisplay_sort") toStore.set('sort', $(this).prop('value'));
        } );
        // selects: how many lines for textinput fields, what to select by default
        $(`${dlg} select`).each( function() {
            let name = $(this).prop('name'), value = $(this).prop('value');

            if (name == "tagsearchdisplay_text") toStore.set('text', value);
            else if (name == "tagsearchdefault_type") toStore.set('deftype', value);
            else if (name == "tagsearchdefault_status") toStore.set('defstatus', value);
            else if (name == "tagsearchdefault_sort") {
                toStore.set('defsortby', value.slice(0, value.indexOf("-")));
                toStore.set('defsortdir', value.slice(value.indexOf("-")+1));
            }
        } );

        // sets the localStorage (turn Map() into an Array for stringify to understand it)
        // btw this overwrites any old configurations, since we're still using the same key name
        localStorage.setItem(cfg, JSON.stringify(toStore.entries().toArray()));

        $( dlg ).dialog( "close" );
        location.reload();
    }

    // --- WRITING THE NEW TAG SEARCH -------------------------------------------------------------------------------

    // for the fields to move/wrap nicely no matter the screen width, we have to group: the two text fields vs. the four selectors (including their respective labels)
    $('#new_tag_search dl > *').slice(0,4).wrapAll('<div id="smallsearch_first"></div>');
    $('#new_tag_search dl > *').slice(1).wrapAll('<div id="smallsearch_second"></div>');

    // general CSS for the fields and flexbox for the four selects
    let custom_css = `
        #fandom-field-description { display: none; }
        #new_tag_search #smallsearch_second { display: flex; flex-flow: row wrap; column-gap: 1rem; row-gap: 0rem; }
        #new_tag_search #smallsearch_second dd { width: auto; }
        #new_tag_search dd li.input { margin: 0; }
        #new_tag_search input[type="text"]::placeholder { opacity: 0.5; font-style: italic; }
        `;

    // one-line display: flexbox to move underneath each other on small screens, hide the appropriate <label>s and their <dt>s
    if (settings.get('text') == "1") {
        $('#new_tag_search dt').hide();
        custom_css += `
            #new_tag_search #smallsearch_first { display: flex; flex-flow: row wrap; column-gap: 2%; row-gap: 0rem; align-items: flex-end; }
            #new_tag_search #smallsearch_first dd { width: 49%; flex-grow: 1; min-width: 15em; }
            `;

        // adding a placeholder text to the <input> fields since the labels are gone
        $('input#tag_search_name').prop('placeholder', 'Tag Name');
        $('input#tag_search_fandoms_autocomplete').prop('placeholder', 'Fandom');
    }
    // two-line display with labels in separate flexboxes, or the second label would always move up into the first row
    else {
        $('#new_tag_search #smallsearch_second dt').hide();

        $('#new_tag_search #smallsearch_first > *').slice(0,2).wrapAll('<div id="smallsearch_firstA"></div>');
        $('#new_tag_search #smallsearch_first > *').slice(1).wrapAll('<div id="smallsearch_firstB"></div>');
        custom_css += `
            #new_tag_search #smallsearch_firstA, #new_tag_search #smallsearch_firstB {
                display: flex; flex-flow: row wrap; column-gap: 1rem; row-gap: 0rem; align-items: flex-end;
            }
            #new_tag_search #smallsearch_first dt { float: none; align-self: start; max-width: 10em; }
            #new_tag_search #smallsearch_first dd { min-width: 15em; width: unset; flex-grow: 1; }
            `;
    }

    $("head").append("<style type='text/css'>" + custom_css + "</style>");

    let labels = settings.get('labels') == "y" ? true : false;
    let btntxt = settings.get('btntxt') == "y" ? true : false;

    // (code readability) calling functions that'll rewrite the choices into buttons or selects, per config
    writeTagTypeChoices();
    writeTagStatusChoices();
    if (settings.get('sort') === "h") {
        // hide the sort by/dir from view (but their original <select> are still there)
        $('#new_tag_search #smallsearch_second dd').slice(2).hide();
        // select the correct <option> in the background so default sort config will still work
        $('#new_tag_search select[name="tag_search[sort_column]"]').find(`option[value="${opt_selected.get("tag_search[sort_column]")}"]`).prop('selected', true);
        $('#new_tag_search select[name="tag_search[sort_direction]"]').find(`option[value="${opt_selected.get("tag_search[sort_direction]")}"]`).prop('selected', true);
    }
    else { // if not hidden, build them as buttons or selects
        writeSortByChoices();
        writeSortDirChoices();
    }

    // --- HELPER FUNCTIONS TO WRITE PAGE HTML -------------------------------------------------------------------------------

    function writeTagTypeChoices() {
        let choices = $('#new_tag_search input[name="tag_search[type]"]');
        let style = settings.get('tag') || "s";
        let html = `<div id="search_type_choice">`;
        if (labels) html += `<label for="tag_search[type]">Tag Type</label><br />`;

        if (style === "b") { // buttons in control group
            let icons = { Fandom: "fa-archive", Character: "fa-user", Relationship: "fa-users", Freeform: "fa-tags", UnsortedTag: "fa-question", Any: "fa-asterisk" };
            $(choices).each(function() {
                html += `<label for="${this.id}" title="${$(this).next().text()}"><i class="fa ${ this.value === "" ? icons.Any : icons[this.value]}"></i>
                    ${btntxt ? $(this).next().text() : "" }</label><input type="radio" id="${this.id}" name="tag_search[type]" value="${this.value}"
                    ${ opt_selected.get("tag_search[type]") === this.value ? 'checked="checked"' : "" }>`;
            });
            html += "</div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(1)').html(html);
            $('input[name="tag_search[type]"]').checkboxradio({ icon: false });
            $('#search_type_choice').controlgroup();
        }
        else if (style === "s") { // dropdown select
            html += `<select name="tag_search[type]" style="width: max-content">`;
            $(choices).each(function() {
                html += `<option value="${this.value}">${$(this).next().text()}</option>`;
            });
            html += "</select></div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(1)').html(html); // write the new <select> to page
            $('#new_tag_search select[name="tag_search[type]"]').find(`option[value="${opt_selected.get("tag_search[type]")}"]`).prop('selected', true); // select the correct <option>
            $('#new_tag_search select[name="tag_search[type]"]').selectmenu({ width: null }); // prettify
        }
    }

    function writeTagStatusChoices() {
        let choices = $('#new_tag_search input[name="tag_search[canonical]"]');
        let style = settings.get('tag') || "s";
        let html = `<div id="search_status_choice">`;
        if (labels) html += `<label for="tag_search[canonical]">Tag Status</label><br />`;

        if (style === "b") { // buttons in control group
            let icons = { T: "fa-check-square", F: "fa-square-o", Any: "fa-asterisk" };
            $(choices).each(function() {
                html += `<label for="${this.id}" title="${$(this).next().text()}"><i class="fa ${ this.value === "" ? icons.Any : icons[this.value]}"></i>
                    ${btntxt ? $(this).next().text() : "" }</label><input type="radio" id="${this.id}" name="tag_search[canonical]" value="${this.value}"
                    ${ opt_selected.get("tag_search[canonical]") === this.value ? 'checked="checked"' : "" }>`;
            });
            html += "</div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(2)').html(html);
            $('input[name="tag_search[canonical]"]').checkboxradio({ icon: false });
            $('#search_status_choice').controlgroup();
        }
        else if (style === "s") { // dropdown select
            html += `<select name="tag_search[canonical]" style="width: max-content">`;
            $(choices).each(function() {
                html += `<option value="${this.value}">${$(this).next().text()}</option>"`;
            });
            html += "</select></div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(2)').html(html); // write the new <select> to page
            $('#new_tag_search select[name="tag_search[canonical]"]').find(`option[value="${opt_selected.get("tag_search[canonical]")}"]`).prop('selected', true); // select the correct <option>
            $('#new_tag_search select[name="tag_search[canonical]"]').selectmenu({ width: null }); // prettify
        }
    }

    function writeSortByChoices() {
        let choices = $('#new_tag_search select[name="tag_search[sort_column]"] option');
        let style = settings.get('sort') || "s";
        let html = `<div id="search_sort_choice">`;

        if (style === "b") { // buttons in control group
            if (labels) html += `<label for="tag_search[sort_column]">Sort By</label><br />`;
            let icons = { name: "fa-font", created_at: "fa-calendar", uses: "fa-bar-chart" };
            $(choices).each(function() {
                html += `<label for="tag_search_sort_${this.value}" title="${$(this).text()}"><i class="fa ${icons[this.value]}"></i>
                    ${btntxt ? $(this).text() : "" }</label><input type="radio" id="tag_search_sort_${this.value}" name="tag_search[sort_column]" value="${this.value}"
                    ${ opt_selected.get("tag_search[sort_column]") === this.value ? 'checked="checked"' : "" }>`;
            });
            html += "</div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(3)').html(html);
            // jQueryUI make it pretty
            $('input[name="tag_search[sort_column]"]').checkboxradio({ icon: false });
            $('#search_sort_choice').controlgroup();
            // change eventhandler (on any <input> = button within this controlgroup) to dynamically update the ASC/DESC labels
            $('#search_sort_choice').on('change', "input", function() { onSortByChange('BUTTON'); });
        }
        else if (style === "s") { // dropdown select
            let select = $('#new_tag_search select[name="tag_search[sort_column]"]').css('width', '15em');
            if (!labels) $(choices).prepend("Sort by "); // add the "sort by" text into the <option>s if the labels are hidden
            else $(select).before(`<label for="tag_search[sort_column]">Sort By</label><br />`);
            $(choices).each(function() {
                if (opt_selected.get("tag_search[sort_column]") === this.value) $(this).prop('selected', true);
            });
            // jQueryUI make it pretty (width null forces original size) - with a change eventhandler to dynamically update the ASC/DESC labels
            $( select ).selectmenu({ width: null, change: function(event, ui) { onSortByChange('SELECT'); } });
        }
    }

    function writeSortDirChoices() {
        let choices = $('#new_tag_search select[name="tag_search[sort_direction]"] option');
        let style = settings.get('sort') || "s";
        let html = `<div id="search_order_choice">`;

        if (style === "b") { // buttons in control group
            if (labels) html += `<label for="tag_search[sort_direction]">Sort Direction</label><br />`;
            $(choices).each(function() {
                let dir_readable = getAscDescAlias($('[name="tag_search[sort_column]"]:checked').prop('value'), this.value); // readable name depends on which sort-by is selected
                let dir_icon = !labels ? getAscDescIcon($('[name="tag_search[sort_column]"]:checked').prop('value'), this.value) : `<i class="fa fa-sort-amount-${this.value}"></i>`;
                html += `<label for="tag_search_sort_${this.value}" title="${dir_readable}">${dir_icon} ${btntxt ? dir_readable : "" }</label>
                    <input type="radio" id="tag_search_sort_${this.value}" name="tag_search[sort_direction]" value="${this.value}"
                    ${ opt_selected.get("tag_search[sort_direction]") === this.value ? 'checked="checked"' : "" }>`;
            });
            html += "</div>";
            $('#new_tag_search #smallsearch_second dd:nth-of-type(4)').html(html);
            // jQueryUI make it pretty
            $('input[name="tag_search[sort_direction]"]').checkboxradio({ icon: false });
            $('#search_order_choice').controlgroup();
        }
        else if (style === "s") { // dropdown select
            let select = $('#new_tag_search select[name="tag_search[sort_direction]"]').css('width', '13em');
            if (labels) $(select).before(`<label for="tag_search[sort_direction]">Sort Direction</label><br />`);
            // change ASC/DESC into something human-readable
            $(select).find('option').each(function() {
                let dir_readable = getAscDescAlias($('[name="tag_search[sort_column]"]').prop('value'), this.value);
                this.innerText = dir_readable;
                if (opt_selected.get("tag_search[sort_direction]") === this.value) $(this).prop('selected', true);
            });
            // jQueryUI make it pretty (width null forces original size)
            $( select ).selectmenu({ width: null });
        }
    }

    // --- DELEGATED EVENT HANDLERS FOR REACTIVE PAGE -------------------------------------------------------------------------------

    // event handler listening to user changing the sort by field so we can update the text on the ASC/DESC
    function onSortByChange(elemType) {
        if (elemType === "SELECT") {
            // grab the now selected sort-by
            let new_sort_by =  $('[name="tag_search[sort_column]"]').prop('value');
            // update the labels of the ASC/DESC on our original form elements
            $('#new_tag_search [name="tag_search[sort_direction]"] option').each(function() {
                let dir_readable = getAscDescAlias(new_sort_by, this.value);
                this.innerText = dir_readable;
            });
            // refresh the jQueryUI elements to show the same new labels
            $('#new_tag_search select[name="tag_search[sort_direction]"]').selectmenu( "refresh" );
        }
        else {
            // grab the now selected sort-by
            let new_sort_by =  $('[name="tag_search[sort_column]"]:checked').prop('value');
            // update the labels of the ASC/DESC on our original form elements
            let dir_readable = getAscDescAlias(new_sort_by, 'asc');
            if (labels) $('#new_tag_search label[for="tag_search_sort_asc"]').prop('title', dir_readable).html(`<i class="fa fa-sort-amount-asc"></i> ${dir_readable}`);
            else $('#new_tag_search label[for="tag_search_sort_asc"]').prop('title', dir_readable).html(getAscDescIcon(new_sort_by, 'asc'));

            dir_readable = getAscDescAlias(new_sort_by, 'desc');
            if (labels) $('#new_tag_search label[for="tag_search_sort_desc"]').prop('title', dir_readable).html(`<i class="fa fa-sort-amount-desc"></i> ${dir_readable}`);
            else $('#new_tag_search label[for="tag_search_sort_desc"]').prop('title', dir_readable).html(getAscDescIcon(new_sort_by, 'desc'));
        }
    }

})(jQuery);