etiquette/frontends/etiquette_flask/templates/search.html
Ethan Dalool 4c65ccaf68 Big! Liberate Groupables from strict heirarchy. Multiple parents.
I found that the strict heirarchy was not satisfying the situation
where one tag is the intersection of two others, but we can only
pick one as the parent

For example, does red_jacket belong under clothes.red_clothes or
clothes.jackets? A search for "red_clothes AND jackets" might
give us someone wearing red pants and a black jacket, so this
definitely needs to be a separate tag, but picking only one
parent for it is not sufficient. Now, a search for red_clothes
and a search for jackets will both find our red_jacket photo.

The change also applies to Albums because why not, and I'm sure
a similar case can be made.

Unfortunately this means tags no longer have one true qualname.
The concept of qualnames has not been completely phased out but
it's in progress.

This commit is very big because I was not sure for a long time
whether to go through with it, and so much stuff had to change
that I don't want to go back and figure out what could be grouped
together.
2018-07-20 13:09:06 -07:00

601 lines
20 KiB
HTML

<!DOCTYPE html5>
<html>
<head>
{% import "photo_card.html" as photo_card %}
{% import "header.html" as header %}
{% import "tag_object.html" as tag_object %}
{% import "clipboard_tray.html" as clipboard_tray %}
<title>Search</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/photo_card.css">
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
<script src="/static/js/common.js"></script>
<script src="/static/js/hotkeys.js"></script>
<script src="/static/js/photoclipboard.js"></script>
<style>
form
{
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
#error_message_area
{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.search_warning
{
align-self: center;
padding: 2px;
background-color: #f00;
color: #fff;
}
#left
{
flex: 1;
padding: 8px;
max-width: 300px;
min-width: 300px;
width: 300px;
background-color: rgba(0, 0, 0, 0.1);
word-wrap: break-word;
}
#right
{
flex: 1;
padding: 8px;
/* Keep the prev-next buttons from scraping the floor */
padding-bottom: 30px;
width: auto;
}
@media screen and (max-width: 800px) {
#content_body
{
flex-direction: column-reverse;
}
#left
{
max-width: none;
width: initial;
}
}
.prev_next_holder
{
display: flex;
flex-direction: row;
}
.prev_page
{
margin-right: 10px;
}
.next_page
{
margin-left: 10px;
}
.prev_page, .next_page
{
display: flex;
flex: 1;
justify-content: center;
border: 1px solid black;
background-color: #ffffd4;
font-size: 20;
}
.search_builder_tagger,
#search_builder_orderby_ul
{
margin: 0;
}
</style>
{% macro prev_next_buttons() %}
{% if prev_page_url or next_page_url %}
<div class="prev_next_holder">
{% if prev_page_url %}
<a class="prev_page" href="{{prev_page_url}}">Previous</a>
{% else %}
<a class="prev_page"><br></a>
{% endif %}
{% if next_page_url %}
<a class="next_page" href="{{next_page_url}}">Next</a>
{% else %}
<a class="next_page"><br></a>
{% endif %}
</div>
{% endif %}
{% endmacro %}
{% macro create_orderby_li(selected_column, selected_sorter) %}
<li class="search_builder_orderby_li">
<select>
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
<option value="height" {%if selected_column=="height"%}selected{%endif%} >Height</option>
<option value="ratio" {%if selected_column=="ratio"%}selected{%endif%} >Aspect Ratio</option>
<option value="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option>
<option value="duration" {%if selected_column=="duration"%}selected{%endif%} >Duration</option>
<option value="tagged_at" {%if selected_column=="tagged_at"%}selected{%endif%}>Recently tagged</option>
<option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option>
</select>
<select>
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
<option value="asc" {%if selected_sorter=="asc"%}selected{%endif%} >Ascending</option>
</select>
<button class="remove_tag_button_perm red_button" onclick="orderby_remove_hook(this);"></button>
</li>
{% endmacro %}
</head>
<body>
{{header.make_header(session=session)}}
<div id="error_message_area">
{% for warning in warnings %}
<span class="search_warning">{{warning}}</span>
{% endfor %}
</div>
<div id="content_body">
<div id="left">
{% for tagtype in ["musts", "mays", "forbids"] %}
<div id="search_builder_{{tagtype}}" {% if search_kwargs["tag_expression"]%}style="display:none"{%endif%}>
<span>Tag {{tagtype}}:</span>
<ul class="search_builder_tagger">
{% set key="tag_" + tagtype %}
{% if search_kwargs[key] %}
{% for tagname in search_kwargs[key] %}
<li class="search_builder_{{tagtype}}_inputted">
<span class="tag_object">{{tagname}}</span>{{-''-}}
<button class="remove_tag_button red_button"
onclick="remove_searchtag(this, '{{tagname}}', inputted_{{tagtype}});"></button>
</li>
{% endfor %}
{% endif %}
<li><input id="search_builder_{{tagtype}}_input" type="text"></li>
</ul>
</div>
{% endfor %}
<div id="search_builder_expression" {% if not search_kwargs["tag_expression"]%}style="display:none"{%endif%}>
<span>Tag Expression:</span>
<input id="search_builder_expression_input" name="tag_expression" type="text"
{% if search_kwargs["tag_expression"] %}
value="{{search_kwargs["tag_expression"]}}"
{% endif %}
>
</div>
<div id="search_builder_orderby">
<span>Order by</span>
<ul id="search_builder_orderby_ul">
{% if "orderby" in search_kwargs and search_kwargs["orderby"] %}
{% for orderby in search_kwargs["orderby"] %}
{% set column, sorter=orderby.split("-") %}
{{ create_orderby_li(selected_column=column, selected_sorter=sorter) }}
{% endfor %}
{% else %}
{{ create_orderby_li(selected_column=0, selected_sorter=0) }}
{% endif %}
<li id="search_builder_orderby_newrow"><button class="green_button" onclick="add_new_orderby()">+</button></li>
</ul>
</div>
<br>
<form id="search_builder_form" action="" onsubmit="return submit_search();">
<span>Min-max values</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['area']%}{{search_kwargs['area']}}{%endif%}"
name="area" placeholder="Area: 1m-2m">
<input type="text" class="basic_param"
value="{%if search_kwargs['width']%}{{search_kwargs['width']}}{%endif%}"
name="width" placeholder="Width: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['height']%}{{search_kwargs['height']}}{%endif%}"
name="height" placeholder="Height: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['ratio']%}{{search_kwargs['ratio']}}{%endif%}"
name="ratio" placeholder="Aspect Ratio: 1.7-2">
<input type="text" class="basic_param"
value="{%if search_kwargs['bytes']%}{{search_kwargs['bytes']}}{%endif%}"
name="bytes" placeholder="File Size: 1mb-2mb">
<input type="text" class="basic_param"
value="{%if search_kwargs['duration']%}{{search_kwargs['duration']}}{%endif%}"
name="duration" placeholder="Duration: 10:00-20:00">
<input type="text" class="basic_param"
value="{%if search_kwargs['created']%}{{search_kwargs['created']}}{%endif%}"
name="created" placeholder="Created: 1483228800-1514764800">
<br>
<span>Other filters</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['filename']%}{{search_kwargs['filename']}}{%endif%}"
name="filename" placeholder="Filename">
<input type="text" class="basic_param"
value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}"
name="mimetype" placeholder="Mimetype(s)">
<input type="text" class="basic_param"
value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}"
name="extension" placeholder="Extension(s)">
<input type="text" class="basic_param"
value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"
name="extension_not" placeholder="Forbid extension(s)">
<input type="text" class="basic_param"
value="{%if search_kwargs['author']%}{{search_kwargs['author']}}{%endif%}"
name="author" placeholder="Author">
<select name="limit" class="basic_param">
{% set limit_options = [20, 50, 100] %}
{% if search_kwargs['limit'] not in limit_options %}
{% do limit_options.append(search_kwargs['limit']) %}
{% do limit_options.sort() %}
{% endif %}
{% for limit_option in limit_options %}
<option{{-' '-}}
value="{{limit_option}}"{{-''-}}
{{-" selected" if search_kwargs['limit'] == limit_option else ""-}}
>
{{- limit_option }} items{{-''-}}
</option>
{% endfor %}
</select>
<select name="has_tags" class="basic_param">
<option value="" {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option>
<option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option>
<option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option>
</select>
<select name="has_thumbnail" class="basic_param">
<option value="" {%if search_kwargs['has_thumbnail'] == None %}selected{%endif%}>Thumbnail doesn't matter</option>
<option value="yes"{%if search_kwargs['has_thumbnail'] == True %}selected{%endif%}>Has thumbnail</option>
<option value="no" {%if search_kwargs['has_thumbnail'] == False %}selected{%endif%}>No thumbnail</option>
</select>
<select name="view" class="basic_param">
<option value="grid" {%if search_kwargs['view'] == "grid" %}selected{%endif%}>Grid</option>
<option value="list" {%if search_kwargs['view'] == "list" %}selected{%endif%}>List</option>
</select>
<button type="submit" id="search_go_button" class="green_button" value="">Search</button>
</form>
{% if total_tags %}
<span>Tags on this page (click to join query):</span>
<ul>
{% for tag in total_tags %}
<li>{{-tag_object.tag_object(
tag,
extra_classes="tags_on_this_page",
link='void',
with_alt_description=True,
)-}}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div id="right">
<p>You got {{photos|length}} items</p>
{{prev_next_buttons()}}
<div id="search_results_holder">
{% for photo in photos %}
{{photo_card.create_photo_card(photo, view=search_kwargs["view"])}}
{% endfor %}
</div>
{{prev_next_buttons()}}
</div>
</div>
{{clipboard_tray.clipboard_tray()}}
</body>
<script type="text/javascript">
/*
These are defined so that we know we don't need to include them in the
constructed search URL, keeping it more tidy.
*/
PARAM_DEFAULTS = {
'limit': 50,
'view': 'grid',
}
function add_searchtag(box, value, inputted_list, li_class)
{
/*
Called by hitting Enter within a must/may/forbid field. Checks whether the
tag exists and adds it to the query.
*/
if (box.offsetParent === null)
{
// The box is hidden probably because we're in Expression mode.
return;
}
console.log("adding " + value);
var already_have = false;
// We're going to be doing some in-place splicing to remove,
// so make a duplicate for iterating
existing_tags = Array.from(inputted_list);
for (var index = 0; index < existing_tags.length; index += 1)
{
existing_tag = existing_tags[index];
if (existing_tag == value)
{
already_have = true;
}
else if (existing_tag.startsWith(value + ".") || value.startsWith(existing_tag + "."))
{
remove_searchtag(box, existing_tag, inputted_list);
}
}
if (!already_have)
{
inputted_list.push(value);
var new_li = document.createElement("li");
new_li.className = li_class;
var new_span = document.createElement("span");
new_span.className = "tag_object";
new_span.innerHTML = value;
var new_delbutton = document.createElement("button")
new_delbutton.classList.add("remove_tag_button");
new_delbutton.classList.add("red_button");
new_delbutton.onclick = function(){remove_searchtag(new_delbutton, value, inputted_list)};
new_li.appendChild(new_span);
new_li.appendChild(new_delbutton);
box_li = box.parentElement;
ul = box_li.parentElement;
ul.insertBefore(new_li, box_li);
}
}
function remove_searchtag(li_member, value, inputted_list)
{
/*
Given a member of the same tag type as the one we intend to remove,
find the tag of interest and remove it from both the DOM and the
inputted_list.
Sorry for the roundabout technique.
*/
console.log("removing " + value);
var li = li_member.parentElement;
var ul = li.parentElement;
var lis = ul.children;
//console.log(lis);
for (var index = 0; index < lis.length; index += 1)
{
li = lis[index];
var span = li.children[0];
if (span.tagName != "SPAN")
{continue}
var tagname = span.innerHTML;
if (tagname != value)
{continue}
ul.removeChild(li);
splice_at = inputted_list.indexOf(tagname);
if (splice_at == -1)
{continue}
inputted_list.splice(splice_at, 1);
}
}
function add_new_orderby()
{
/* Called by the green + button */
var ul = document.getElementById("search_builder_orderby_ul");
if (ul.children.length >= 9)
{
/* 9 because there are only 9 sortable properties */
return;
}
var li = ul.children;
li = li[li.length - 2];
var clone_children = true;
var new_li = li.cloneNode(clone_children)
var button = document.getElementById("search_builder_orderby_newrow");
ul.insertBefore(new_li, button);
}
function orderby_remove_hook(button)
{
/* Called by the red button next to orderby dropdowns */
var li = button.parentElement;
var ul = li.parentElement;
// 2 because keep 1 row and the adder button
if (ul.children.length>2)
{
/* You can't remove the only one left */
ul.removeChild(li);
}
}
function simplify_tagnames(tags)
{
var new_tags = [];
for (var index = 0; index < tags.length; index += 1)
{
var tag = tags[index];
tag = tag.split(".");
tag = tag[tag.length - 1];
new_tags.push(tag);
}
return new_tags;
}
function submit_search()
{
/*
Gather up all the form data and tags and compose the search URL
*/
var url = window.location.origin + "/search";
var parameters = [];
var has_tag_params = false;
var musts = simplify_tagnames(inputted_musts).join(",");
if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;}
var mays = simplify_tagnames(inputted_mays).join(",");
if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;}
var forbids = simplify_tagnames(inputted_forbids).join(",");
if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;}
var expression = document.getElementsByName("tag_expression")[0].value;
if (expression)
{
//expression = expression.replace(new RegExp(" ", 'g'), "-");
parameters.push("tag_expression=" + expression);
has_tag_params=true;
}
var basic_inputs = document.getElementsByClassName("basic_param");
for (var index = 0; index < basic_inputs.length; index += 1)
{
var boxname = basic_inputs[index].name;
var box = document.getElementsByName(boxname)[0];
var value = box.value;
value = value.split("&").join("%26");
console.log(value);
if (PARAM_DEFAULTS[boxname] == value)
{
// Don't clutter url with default values.
continue;
}
if (value == "")
{
continue;
}
parameters.push(boxname + "=" + value);
}
orderby_rows = document.getElementsByClassName("search_builder_orderby_li");
orderby_params = [];
for (var index = 0; index < orderby_rows.length; index += 1)
{
var row = orderby_rows[index];
var column = row.children[0].value;
var sorter = row.children[1].value;
orderby_params.push(column + "-" + sorter);
}
orderby_params = orderby_params.join(",");
if (orderby_params && orderby_params != "created-desc")
{
// Don't clutter url with default of created-desc
parameters.push("orderby=" + orderby_params);
}
if (parameters.length > 0)
{
parameters = parameters.join("&");
parameters = "?" + parameters;
url = url + parameters;
}
console.log(url);
window.location.href = url;
return false;
}
function tags_on_this_page_hook()
{
/*
This is hooked onto the tag objects listed under "Found on this page".
Clicking them will add it to your current search query under Musts
*/
var tagname = this.innerHTML.split(/\./);
tagname = tagname[tagname.length-1];
var qualname = ALL_TAGS[tagname];
add_searchtag(
input_musts,
qualname,
inputted_musts,
"search_builder_musts_inputted"
);
return false;
}
function tag_input_hook(box, inputted_list, li_class)
{
/*
Assigned to the input boxes for musts, mays, forbids.
Hitting Enter will add the resovled tag to the search form.
*/
if (event.keyCode != 13)
{return;}
if (!box.value)
{return;}
var value = box.value.toLocaleLowerCase();
value = value.split(".");
value = value[value.length-1];
value = value.split("+")[0];
value = value.replace(new RegExp(" ", 'g'), "_");
value = value.replace(new RegExp("-", 'g'), "_");
if (ALL_TAGS.indexOf(value) == -1)
{
return;
}
console.log(inputted_list);
add_searchtag(box, value, inputted_list, li_class)
box.value = "";
}
ALL_TAGS = {{all_tags|safe}};
var input_musts = document.getElementById("search_builder_musts_input");
var input_mays = document.getElementById("search_builder_mays_input");
var input_forbids = document.getElementById("search_builder_forbids_input");
var input_expression = document.getElementById("search_builder_expression_input");
/* Prefix the form with the parameters from last search */
var inputted_musts = [];
var inputted_mays = [];
var inputted_forbids = [];
{% for tagtype in ["musts", "mays", "forbids"] %}
{% set key="tag_" + tagtype %}
{% if search_kwargs[key] %}
{% for tagname in search_kwargs[key] %}
inputted_{{tagtype}}.push("{{tagname}}");
{% endfor %}
{% endif %}
{% endfor %}
/* Assign the click handler to "Tags on this page" results. */
var found_on_page = document.getElementsByClassName("tags_on_this_page");
for (var index = 0; index < found_on_page.length; index += 1)
{
var tag_object = found_on_page[index];
if (tag_object.tagName != "A")
{continue}
tag_object.onclick = tags_on_this_page_hook;
}
input_musts.addEventListener("keyup", function(){tag_input_hook(this, inputted_musts, "search_builder_musts_inputted")});
input_mays.addEventListener("keyup", function(){tag_input_hook(this, inputted_mays, "search_builder_mays_inputted")});
input_forbids.addEventListener("keyup", function(){tag_input_hook(this, inputted_forbids, "search_builder_forbids_inputted")});
bind_box_to_button(input_expression, document.getElementById("search_go_button"));
</script>
</html>