diff --git a/the_works/static/js/init_dt.js b/the_works/static/js/init_dt.js new file mode 100644 index 0000000..ab6e160 --- /dev/null +++ b/the_works/static/js/init_dt.js @@ -0,0 +1,38 @@ +/* + * DATATABLES FUNCTIONS + */ +// initializes the DataTable functionality for the given table +function initDataTable(table_id) { + // add # to id if not there already + table_id = table_id.slice(0, 1) == "#" ? table_id : "#" + table_id + // initialize table + let table = new DataTable(table_id, { + paging: false, + order: [] + }); + // remove role from inside th elements to avoid clashing with PicoCSS + spans = document.querySelectorAll(table_id + " th span.dt-column-order") + .forEach( function(el) { + el.removeAttribute("role"); + } + ); +} + +// create "New"-element and append it to the
containing the DataTables search field +function initCreateButton(table_id, title, href=null) { + // build button element + let button = document.createElement("button"); + button.id = "create-button"; + button.setAttribute("title", title); + button.textContent = "Neu …"; + // wrap button inside an if href is given + if ( href ) { + let b = button; + button = document.createElement("a"); + button.setAttribute("href", href); + button.appendChild(b); + } + // insert element + document.getElementById(`${table_id.slice(0, 1) == "#" ? table_id.slice(1) : table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(button); +} + diff --git a/the_works/static/js/modal.js b/the_works/static/js/modal.js new file mode 100644 index 0000000..20745ad --- /dev/null +++ b/the_works/static/js/modal.js @@ -0,0 +1,41 @@ +/* + * MODAL FUNCTIONS + */ +// adds event handlers that raise a modal +function initModal(modal_id, input_ids, headings, form_actions) { + // if input_ids is not an array, make it a single-element array + input_ids = Array.isArray(input_ids) ? input_ids : [ input_ids ]; + // add event listener to "New" element + 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 + for (const el of document.querySelectorAll('.action-update')) { +console.log(`input_ids is ${input_ids}`); + 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 + )); + } +} + + +// raises a modal with the given options +function showDialog(modal_id, input_ids, heading, form_action, input_values, url_id) { +console.dir(arguments); + // if form action includes the string "update", the id at the end of the URL is a dummy and must be replaced with the correct id + if ( form_action.includes("update") && url_id) { + form_action = form_action.slice(0, form_action.lastIndexOf("/") + 1) + url_id; + } + // set modal attributes + document.getElementById("dialog-heading").textContent = heading; + document.getElementById("form_submit").formAction = form_action; + for (var i = 0; i < input_ids.length; i++ ) { + document.getElementById(input_ids[i]).value = input_values[i] || ""; + } + // raise modal + document.getElementById(modal_id).showModal(); +} + diff --git a/the_works/static/js/multiselect.js b/the_works/static/js/multiselect.js new file mode 100644 index 0000000..cef5868 --- /dev/null +++ b/the_works/static/js/multiselect.js @@ -0,0 +1,104 @@ +/* + * MULTISELECT FUNCTIONS + */ +// initializes all dropdown selects on the page +function initAllMultiselects() { + // initialize each dropdown individually + let dropdowns = document.querySelectorAll("form details.dropdown"); + dropdowns.forEach(d => initMultiselect(d)); + // now it's time to set the z-indices + // get the_works style sheet + let sheet = [...document.styleSheets].filter(s => s.ownerNode.getAttribute("href").includes("the_works"))[0]; + const initialZ = 20; + // add style rules to set z-indices on dropdown elements + // using reverse order of dropdown elements on page; this way a dropdown thaht's higher on the page will overlap a lower one + for ( let i = 0; i < dropdowns.length; i++ ) { + sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} { z-index: ${initialZ + i*3};}`, sheet.cssRules.length); + sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} .dropdown-badge { z-index: ${initialZ+1+i*3};}`, sheet.cssRules.length); + sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} .dropdown-badge-close { z-index: ${initialZ+2+i*3};}`, sheet.cssRules.length); + } + // event handler for summary elements (see below) + function summaryClick(event, that) { + if ( event.target != that && that.contains(event.target) ) { + event.preventDefault(); + } + } + // 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)); +} + + +// initialize a dropdown select component +function initMultiselect(dropdown) { + // empty search field + let search = dropdown.querySelector("input[type='search']"); + search.value = ""; + // add event listener to search field + let input_name = dropdown.querySelector("input[type='checkbox']").getAttribute("name"); + search.addEventListener("input", event => { + filterMultiselectChoices(event.target, input_name); + }); + // add event listener to checkboxes + let summary = dropdown.querySelector("summary"); + document.querySelectorAll(`[name='${input_name}']`).forEach( el => { + el.addEventListener("change", event => { + updateMultiselectSelected(event.target, summary); + }); + // add badges for genre that were already selected + if ( el.checked ) { + el.dispatchEvent(new Event("change")); + } + }); +} + +// event handler for dropdown search field; called whenever the search field's input value is changed +function filterMultiselectChoices(that, checkbox_name) { + // show everything when search field is empty + if ( ! that.value || that.value == "" ) { + that.parentNode.parentNode.querySelectorAll("[name='" + checkbox_name + "']").forEach( (el) => { + el.parentNode.parentNode.classList.remove("filtered-out"); + }) + } else { + // iterate over genre inputs + that.parentNode.parentNode.querySelectorAll("[name='" + checkbox_name + "']").forEach( (el) => { + // show/hide genre entry depending on whether genre name includes search string + let noHits = true; + if ( el.parentNode.textContent.toLowerCase().includes(that.value.toLowerCase()) ) { + el.parentNode.parentNode.classList.remove("filtered-out"); + noHits = false; + } else { + el.parentNode.parentNode.classList.add("filtered-out"); + } + }); + } +} + +// event handler for dropdown checkboxes; called whenever a checkbox is checked or unchecked +function updateMultiselectSelected(that, summary) { + if ( that.checked ) { + // add badge when checkbox was checked + let badge = document.createElement("span"); + badge.id = that.id + "-badge"; + badge.classList.add("dropdown-badge"); + badge.textContent = that.parentNode.textContent; + summary.appendChild(badge); + // add closing X to badge + let badgeX = document.createElement("span"); + badgeX.classList.add("dropdown-badge-close"); + badgeX.innerHTML = "×"; + badge.appendChild(badgeX); + badgeX.addEventListener("click", removeMultiselectBadge); + } else { + // remove badge when checkbox was unchecked + document.getElementById(that.id + "-badge").remove(); + } +} + +// unselect a previously checked checkbox by clicking its badge's "X" +function removeMultiselectBadge(event) { + let input_id = this.parentNode.id.slice(0, -6); + document.getElementById(input_id).checked = false; + this.parentNode.remove(); + event.stopPropagation(); +} + diff --git a/the_works/static/js/searchall.js b/the_works/static/js/searchall.js new file mode 100644 index 0000000..9d99c48 --- /dev/null +++ b/the_works/static/js/searchall.js @@ -0,0 +1,80 @@ +/* + * SEARCH-ALL FUNCTIONS + */ +function search_all(s, 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"}); + + 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, `${s}`) : ""; + tr.appendChild(td); + } + tbody.appendChild(tr); + } + table.appendChild(tbody); + heading.innerHTML = `

${db_table} (${tbody.childNodes.length} Treffer)

`; + + // add container element to DOM + container.append(table); + document.getElementById("results").appendChild(container); + + // open
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); + } + }) +} + diff --git a/the_works/static/js/validate_date.js b/the_works/static/js/validate_date.js new file mode 100644 index 0000000..66c413d --- /dev/null +++ b/the_works/static/js/validate_date.js @@ -0,0 +1,27 @@ +/* + * 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"); + 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(); + } + } +} + diff --git a/the_works/static/the_works.js b/the_works/static/the_works.js deleted file mode 100644 index 89eb006..0000000 --- a/the_works/static/the_works.js +++ /dev/null @@ -1,286 +0,0 @@ -/* - * DATATABLES FUNCTIONS - */ -// initializes the DataTable fumctionality for the given table -function initDataTable(table_id) { - // add # to id if not there already - table_id = table_id.slice(0, 1) == "#" ? table_id : "#" + table_id - // initialize table - let table = new DataTable(table_id, { - paging: false, - order: [] - }); - // remove role from inside th elements to avoid clashing with PicoCSS - spans = document.querySelectorAll(table_id + " th span.dt-column-order") - .forEach( function(el) { - el.removeAttribute("role"); - } - ); -} - -// create "New"-element and append it to the
containing the DataTables search field -function initCreateButton(table_id, title, href=null) { - // build button element - let button = document.createElement("button"); - button.id = "create-button"; - button.setAttribute("title", title); - button.textContent = "Neu …"; - // wrap button inside an if href is given - if ( href ) { - let b = button; - button = document.createElement("a"); - button.setAttribute("href", href); - button.appendChild(b); - } - // insert element - 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 -function initModal(modal_id, input_ids, headings, form_actions) { - // if input_ids is not an array, make it a single-element array - input_ids = Array.isArray(input_ids) ? input_ids : [ input_ids ]; - // add event listener to "New" element - 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 - 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)); - } -} - -// raises a modal with the given options -function showDialog(modal_id, input_ids, heading, form_action, input_values, url_id) { - // if form action includes the string "update", the id at the end of the URL is a dummy and must be replaced with the correct id - if ( form_action.includes("update") && url_id) { - form_action = form_action.slice(0, form_action.lastIndexOf("/") + 1) + url_id; - } - // set modal attributes - document.getElementById("dialog-heading").textContent = heading; - document.getElementById("form_submit").formAction = form_action; - for (var i = 0; i < input_ids.length; i++ ) { - document.getElementById(input_ids[i]).value = input_values[i] || ""; - } - // raise modal - document.getElementById(modal_id).showModal(); -} - - -/* - * MULTISELECT FUNCTIONS - */ -// initialize a dropdown select component -function initDropdownSelect(dropdown) { - // empty search field - let search = dropdown.querySelector("input[type='search']"); - search.value = ""; - // add event listener to search field - let input_name = dropdown.querySelector("input[type='checkbox']").getAttribute("name"); - search.addEventListener("input", event => { - filterDropdownChoices(event.target, input_name); - }); - // add event listener to checkboxes - let summary = dropdown.querySelector("summary"); - document.querySelectorAll(`[name='${input_name}']`).forEach( el => { - el.addEventListener("change", event => { - updateDropdownSelected(event.target, summary); - }); - // add badges for genre that were already selected - if ( el.checked ) { - el.dispatchEvent(new Event("change")); - } - }); -} - -// event handler for dropdown search field; called whenever the search field's input value is changed -function filterDropdownChoices(that, checkbox_name) { - // show everything when search field is empty - if ( ! that.value || that.value == "" ) { - that.parentNode.parentNode.querySelectorAll("[name='" + checkbox_name + "']").forEach( (el) => { - el.parentNode.parentNode.classList.remove("filtered-out"); - }) - } else { - // iterate over genre inputs - that.parentNode.parentNode.querySelectorAll("[name='" + checkbox_name + "']").forEach( (el) => { - // show/hide genre entry depending on whether genre name includes search string - let noHits = true; - if ( el.parentNode.textContent.toLowerCase().includes(that.value.toLowerCase()) ) { - el.parentNode.parentNode.classList.remove("filtered-out"); - noHits = false; - } else { - el.parentNode.parentNode.classList.add("filtered-out"); - } - if ( noHits ) { -console.log("everything has been filtered out") - } - }); - } -} - -// event handler for dropdown checkboxes; called whenever a checkbox is checked or unchecked -function updateDropdownSelected(that, summary) { - if ( that.checked ) { - // add badge when checkbox was checked - let badge = document.createElement("span"); - badge.id = that.id + "-badge"; - badge.classList.add("dropdown-badge"); - badge.textContent = that.parentNode.textContent; - summary.appendChild(badge); - // add closing X to badge - let badgeX = document.createElement("span"); - badgeX.classList.add("dropdown-badge-close"); - badgeX.innerHTML = "×"; - badge.appendChild(badgeX); - badgeX.addEventListener("click", removeDropdownBadge); - } else { - // remove badge when checkbox was unchecked - document.getElementById(that.id + "-badge").remove(); - } -} - -// unselect a previously checked checkbox by clicking its badge's "X" -function removeDropdownBadge(event) { - let input_id = this.parentNode.id.slice(0, -6); - document.getElementById(input_id).checked = false; - this.parentNode.remove(); - event.stopPropagation(); -} - -// initializes all dropdown selects on the page -function initAllDropdownSelects() { - // initialize each dropdown individually - let dropdowns = document.querySelectorAll("form details.dropdown"); - dropdowns.forEach(d => initDropdownSelect(d)); - // now it's time to set the z-indices - // get the_works style sheet - let sheet = [...document.styleSheets].filter(s => s.ownerNode.getAttribute("href").includes("the_works"))[0]; - const initialZ = 20; - // set z-index on dropdowns in reverse order - for ( let i = 0; i < dropdowns.length; i++ ) { - sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} { z-index: ${initialZ + i*3};}`, sheet.cssRules.length); - sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} .dropdown-badge { z-index: ${initialZ+1+i*3};}`, sheet.cssRules.length); - sheet.insertRule(`#${dropdowns[dropdowns.length-i-1].id} .dropdown-badge-close { z-index: ${initialZ+2+i*3};}`, sheet.cssRules.length); - } - // event handler for summary elements (see below) - function summaryClick(event, that) { - if ( event.target != that && that.contains(event.target) ) { - event.preventDefault(); - } - } - // 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)); -} - - -/* - * 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, `${s}`) : ""; - tr.appendChild(td); - } - tbody.appendChild(tr); - } - table.appendChild(tbody); - heading.innerHTML = `

${db_table} (${tbody.childNodes.length} Treffer)

`; - - // add container element to DOM - container.append(table); - document.getElementById("results").appendChild(container); - - // open
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"); - 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(); - } - } -} -