// ==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'; }
}