built a search field for the homepage to search the full database (every text cell in every table); toggleable case sensitivity; results shown as accordion
This commit is contained in:
parent
54c8b11ae3
commit
e70ccd9282
@ -1,3 +1,6 @@
|
|||||||
|
/*
|
||||||
|
* DATATABLES FUNCTIONS
|
||||||
|
*/
|
||||||
// initializes the DataTable fumctionality for the given table
|
// initializes the DataTable fumctionality for the given table
|
||||||
function initDataTable(table_id) {
|
function initDataTable(table_id) {
|
||||||
// add # to id if not there already
|
// add # to id if not there already
|
||||||
@ -33,6 +36,10 @@ function initCreateButton(table_id, title, href=null) {
|
|||||||
document.getElementById(`${table_id.slice(0, 1) == "#" ? table_id.slice(1) : table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(button);
|
document.getElementById(`${table_id.slice(0, 1) == "#" ? table_id.slice(1) : table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MODAL FUNCTIONS
|
||||||
|
*/
|
||||||
// adds event handlers that raise a modal
|
// adds event handlers that raise a modal
|
||||||
function initModal(modal_id, input_ids, headings, form_actions) {
|
function initModal(modal_id, input_ids, headings, form_actions) {
|
||||||
// if input_ids is not an array, make it a single-element array
|
// if input_ids is not an array, make it a single-element array
|
||||||
@ -41,7 +48,7 @@ function initModal(modal_id, input_ids, headings, form_actions) {
|
|||||||
document.getElementById("create-button").addEventListener("click", () => showDialog(modal_id, input_ids, headings[0], form_actions[0], new Array(input_ids.length).fill(""), 0));
|
document.getElementById("create-button").addEventListener("click", () => showDialog(modal_id, input_ids, headings[0], form_actions[0], new Array(input_ids.length).fill(""), 0));
|
||||||
// add event listeners to "update" elements
|
// add event listeners to "update" elements
|
||||||
for (const el of document.querySelectorAll('.action-update')) {
|
for (const el of document.querySelectorAll('.action-update')) {
|
||||||
el.addEventListener("click", (event) => showDialog(modal_id, input_ids, headings[1], form_actions[1], input_ids.map((input) => event.currentTarget.dataset[input.slice(5).toLowerCase()]), event.currentTarget.dataset.id));
|
el.addEventListener("click", event => showDialog(modal_id, input_ids, headings[1], form_actions[1], input_ids.map(input => event.currentTarget.dataset[input.slice(5).toLowerCase()]), event.currentTarget.dataset.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,31 +68,10 @@ function showDialog(modal_id, input_ids, heading, form_action, input_values, url
|
|||||||
document.getElementById(modal_id).showModal();
|
document.getElementById(modal_id).showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// validates a date to be of format YYYY, MM-YYYY, DD-MM-YYYY, or empty
|
|
||||||
function validate_date(tag_id="form_Erscheinungstag", monat_id="form_Erscheinungsmonat", jahr_id="form_Erscheinungsjahr") {
|
|
||||||
let t = document.getElementById(tag_id);
|
|
||||||
let m = document.getElementById(monat_id);
|
|
||||||
let j = document.getElementById(jahr_id);
|
|
||||||
t.setCustomValidity("");
|
|
||||||
t.setAttribute("aria-invalid", "false");
|
|
||||||
m.setCustomValidity("");
|
|
||||||
m.setAttribute("aria-invalid", "false");
|
|
||||||
console.log("This is function validate_date(): tag/monat/jahr is " + t.value + "/" + m.value + "/" + j.value); //DEBUG
|
|
||||||
if ( t.value != "" ) {
|
|
||||||
if ( j.value == "" || m.value == "" ) {
|
|
||||||
t.setCustomValidity("wenn der Tag angegeben ist, müssen Monat und Jahr ebenfalls angegeben sein");
|
|
||||||
t.setAttribute("aria-invalid", "true");
|
|
||||||
t.reportValidity();
|
|
||||||
}
|
|
||||||
} else if ( m.value != "" ) {
|
|
||||||
if ( j.value == "") {
|
|
||||||
m.setCustomValidity("wenn der Monat angegeben ist, muss das Jahr ebenfalls angegeben sein");
|
|
||||||
m.setAttribute("aria-invalid", "true");
|
|
||||||
m.reportValidity();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MULTISELECT FUNCTIONS
|
||||||
|
*/
|
||||||
// initialize a dropdown select component
|
// initialize a dropdown select component
|
||||||
function initDropdownSelect(dropdown) {
|
function initDropdownSelect(dropdown) {
|
||||||
// empty search field
|
// empty search field
|
||||||
@ -93,13 +79,13 @@ function initDropdownSelect(dropdown) {
|
|||||||
search.value = "";
|
search.value = "";
|
||||||
// add event listener to search field
|
// add event listener to search field
|
||||||
let input_name = dropdown.querySelector("input[type='checkbox']").getAttribute("name");
|
let input_name = dropdown.querySelector("input[type='checkbox']").getAttribute("name");
|
||||||
search.addEventListener("input", (event) => {
|
search.addEventListener("input", event => {
|
||||||
filterDropdownChoices(event.target, input_name);
|
filterDropdownChoices(event.target, input_name);
|
||||||
});
|
});
|
||||||
// add event listener to checkboxes
|
// add event listener to checkboxes
|
||||||
let summary = dropdown.querySelector("summary");
|
let summary = dropdown.querySelector("summary");
|
||||||
document.querySelectorAll(`[name='${input_name}']`).forEach( (el) => {
|
document.querySelectorAll(`[name='${input_name}']`).forEach( el => {
|
||||||
el.addEventListener("change", (event) => {
|
el.addEventListener("change", event => {
|
||||||
updateDropdownSelected(event.target, summary);
|
updateDropdownSelected(event.target, summary);
|
||||||
});
|
});
|
||||||
// add badges for genre that were already selected
|
// add badges for genre that were already selected
|
||||||
@ -109,7 +95,7 @@ function initDropdownSelect(dropdown) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// event handler for genre search field; called whenever the search field's input value is changed
|
// event handler for dropdown search field; called whenever the search field's input value is changed
|
||||||
function filterDropdownChoices(that, checkbox_name) {
|
function filterDropdownChoices(that, checkbox_name) {
|
||||||
// show everything when search field is empty
|
// show everything when search field is empty
|
||||||
if ( ! that.value || that.value == "" ) {
|
if ( ! that.value || that.value == "" ) {
|
||||||
@ -167,10 +153,10 @@ function removeDropdownBadge(event) {
|
|||||||
function initAllDropdownSelects() {
|
function initAllDropdownSelects() {
|
||||||
// initialize each dropdown individually
|
// initialize each dropdown individually
|
||||||
let dropdowns = document.querySelectorAll("form details.dropdown");
|
let dropdowns = document.querySelectorAll("form details.dropdown");
|
||||||
dropdowns.forEach((d) => initDropdownSelect(d));
|
dropdowns.forEach(d => initDropdownSelect(d));
|
||||||
// now it's time to set the z-indices
|
// now it's time to set the z-indices
|
||||||
// get the_works style sheet
|
// get the_works style sheet
|
||||||
let sheet = [...document.styleSheets].filter((s) => s.ownerNode.getAttribute("href").includes("the_works"))[0];
|
let sheet = [...document.styleSheets].filter(s => s.ownerNode.getAttribute("href").includes("the_works"))[0];
|
||||||
const initialZ = 20;
|
const initialZ = 20;
|
||||||
// set z-index on dropdowns in reverse order
|
// set z-index on dropdowns in reverse order
|
||||||
for ( let i = 0; i < dropdowns.length; i++ ) {
|
for ( let i = 0; i < dropdowns.length; i++ ) {
|
||||||
@ -185,6 +171,117 @@ function initAllDropdownSelects() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// add event handler to summary elements preventing default behavior (toggling the details element) if the click actually targeted a badge
|
// add event handler to summary elements preventing default behavior (toggling the details element) if the click actually targeted a badge
|
||||||
document.querySelectorAll("form details.dropdown summary").forEach((summary) => summary.addEventListener("click", (event) => summaryClick(event, summary), true));
|
document.querySelectorAll("form details.dropdown summary").forEach(summary => summary.addEventListener("click", event => summaryClick(event, summary), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SEARCH-ALL FUNCTIONS
|
||||||
|
*/
|
||||||
|
function search_all(s, url) {
|
||||||
|
console.log(`JavaScript function search_all was called with search string ${s} and url ${url}`);
|
||||||
|
// remove previous results
|
||||||
|
document.getElementById("results").innerHTML = "";
|
||||||
|
if ( s == "" ) { return; }
|
||||||
|
|
||||||
|
// fetch search results
|
||||||
|
let fetch_url = new URL(url);
|
||||||
|
fetch_url.search = new URLSearchParams({query: s, case: document.getElementById("match_case").checked ? "match" : "no"});
|
||||||
|
console.log(`fetch_url is ${fetch_url}`);
|
||||||
|
|
||||||
|
fetch(fetch_url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// tell user if there are no results
|
||||||
|
if ( Object.keys(data).length === 0 ) {
|
||||||
|
let h3 = document.createElement("h3");
|
||||||
|
h3.innerText = "Die Suchanfrage ergab keine Treffer.";
|
||||||
|
document.getElementById("results").appendChild(h3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over search results
|
||||||
|
for ( const db_table of Object.keys(data) ) {
|
||||||
|
let columns = Object.keys(data[db_table][0]);
|
||||||
|
|
||||||
|
// build container, heading, table
|
||||||
|
let container = document.createElement("details");
|
||||||
|
container.classList.add("result-container");
|
||||||
|
container.setAttribute("name", "result-accordion");
|
||||||
|
let heading = document.createElement("summary");
|
||||||
|
container.appendChild(heading);
|
||||||
|
let table = document.createElement("table");
|
||||||
|
|
||||||
|
// build table head
|
||||||
|
let thead = document.createElement("thead");
|
||||||
|
let tr, th, td;
|
||||||
|
tr = document.createElement("tr");
|
||||||
|
for ( const column of columns) {
|
||||||
|
if ( column == "ID" ) { continue; }
|
||||||
|
th = document.createElement("th");
|
||||||
|
th.innerText = column;
|
||||||
|
tr.appendChild(th);
|
||||||
|
}
|
||||||
|
thead.appendChild(tr);
|
||||||
|
table.appendChild(thead);
|
||||||
|
|
||||||
|
// build table body
|
||||||
|
let tbody = document.createElement("tbody");
|
||||||
|
for ( const row of data[db_table] ) {
|
||||||
|
tr = document.createElement("tr");
|
||||||
|
for ( const column of columns ) {
|
||||||
|
// add column "ID" as data attribute, all other columns as regular table columns
|
||||||
|
if ( column == "ID" ) {
|
||||||
|
tr.setAttribute(`data-${db_table}-id`, row[column].toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
td = document.createElement("td");
|
||||||
|
td.innerHTML = row[column] ? row[column].toString().replace(s, `<mark>${s}</mark>`) : "";
|
||||||
|
tr.appendChild(td);
|
||||||
|
}
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
table.appendChild(tbody);
|
||||||
|
heading.innerHTML = `<h3>${db_table} (${tbody.childNodes.length} Treffer)</h3>`;
|
||||||
|
|
||||||
|
// add container element to DOM
|
||||||
|
container.append(table);
|
||||||
|
document.getElementById("results").appendChild(container);
|
||||||
|
|
||||||
|
// open <details> but only if it's the first element
|
||||||
|
if ( document.getElementById("results").children[0] == container ) {
|
||||||
|
container.setAttribute("open", "open");
|
||||||
|
}
|
||||||
|
document.getElementById("results").insertBefore(document.createElement("hr"), container);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MISC FUNCTIONS
|
||||||
|
*/
|
||||||
|
// validates a date to be of format YYYY, MM-YYYY, DD-MM-YYYY, or empty
|
||||||
|
function validate_date(tag_id="form_Erscheinungstag", monat_id="form_Erscheinungsmonat", jahr_id="form_Erscheinungsjahr") {
|
||||||
|
let t = document.getElementById(tag_id);
|
||||||
|
let m = document.getElementById(monat_id);
|
||||||
|
let j = document.getElementById(jahr_id);
|
||||||
|
t.setCustomValidity("");
|
||||||
|
t.setAttribute("aria-invalid", "false");
|
||||||
|
m.setCustomValidity("");
|
||||||
|
m.setAttribute("aria-invalid", "false");
|
||||||
|
console.log("This is function validate_date(): tag/monat/jahr is " + t.value + "/" + m.value + "/" + j.value); //DEBUG
|
||||||
|
if ( t.value != "" ) {
|
||||||
|
if ( j.value == "" || m.value == "" ) {
|
||||||
|
t.setCustomValidity("wenn der Tag angegeben ist, müssen Monat und Jahr ebenfalls angegeben sein");
|
||||||
|
t.setAttribute("aria-invalid", "true");
|
||||||
|
t.reportValidity();
|
||||||
|
}
|
||||||
|
} else if ( m.value != "" ) {
|
||||||
|
if ( j.value == "") {
|
||||||
|
m.setCustomValidity("wenn der Monat angegeben ist, muss das Jahr ebenfalls angegeben sein");
|
||||||
|
m.setAttribute("aria-invalid", "true");
|
||||||
|
m.reportValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,36 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Home{% endblock title %}
|
{% block title %}Alles durchsuchen{% endblock title %}
|
||||||
|
|
||||||
{% block heading %}Home{% endblock heading %}
|
{% block head %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='datatables.css') }}">
|
||||||
|
{% endblock head %}
|
||||||
|
|
||||||
|
{% block heading %}Alles durchsuchen{% endblock heading %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p><a href="{{ url_for('text.all') }}">Alle Texte</a></p>
|
|
||||||
|
{% include "_icons.svg" %}
|
||||||
|
<section>
|
||||||
|
<label>
|
||||||
|
<input type="search" id="search_all" aria-label="search" placeholder="Suchbegriff eingeben …" value="" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Groß-/Kleinschreibung beachten
|
||||||
|
<input type="checkbox" id="match_case" checked />
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="results">
|
||||||
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script src="{{ url_for('static', filename='the_works.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
const url = "{{ url_for('home.search_all', _external=True) }}";
|
||||||
|
window.onload = () => {
|
||||||
|
document.getElementById("search_all").addEventListener("input", event => search_all(event.target.value, url));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{% endblock script %}
|
||||||
@ -1,8 +1,55 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.sql.sqltypes import TEXT
|
||||||
|
from the_works.database import db
|
||||||
|
import the_works.models
|
||||||
|
import inspect
|
||||||
|
|
||||||
bp = Blueprint("home", __name__)
|
bp = Blueprint("home", __name__)
|
||||||
|
|
||||||
|
# prepare list of DB table classes to be searched by search_all()
|
||||||
|
tables = []
|
||||||
|
for name, obj in inspect.getmembers(the_works.models):
|
||||||
|
if "_" not in name and inspect.isclass(obj):
|
||||||
|
tables.append(obj)
|
||||||
|
#print(tables) #DEBUG
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def home():
|
def startpage():
|
||||||
return render_template("views/home.html")
|
return render_template("views/home.html")
|
||||||
|
|
||||||
|
@bp.route("/search")
|
||||||
|
def search_all():
|
||||||
|
print(f"home.search_all(): request.url is {request.url}") #DEBUG
|
||||||
|
# return when query is empty
|
||||||
|
if not request.args.get("query"):
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
# get URL parameters
|
||||||
|
s = request.args.get("query")
|
||||||
|
matchCase = True if request.args.get("case").lower() == "match" else False
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# loop over database tables
|
||||||
|
for table in tables:
|
||||||
|
text_columns = [column.key for column in table.__table__.columns if type(column.type) == TEXT]
|
||||||
|
hits = []
|
||||||
|
# loop over table rows
|
||||||
|
for row in db.session.execute(select(table)):
|
||||||
|
# loop over each text column in row
|
||||||
|
for column in text_columns:
|
||||||
|
if row[0].__getattribute__(column) is None:
|
||||||
|
continue
|
||||||
|
if matchCase:
|
||||||
|
if s in row[0].__getattribute__(column):
|
||||||
|
hits.append(row[0]._asdict())
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if s.lower() in row[0].__getattribute__(column).lower():
|
||||||
|
hits.append(row[0]._asdict())
|
||||||
|
break
|
||||||
|
if hits != []:
|
||||||
|
result[table.__table__.fullname] = hits
|
||||||
|
# return results
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user