From dcafefadf6fcd716481ebfe22e5bb55d07b2547a Mon Sep 17 00:00:00 2001 From: eclipse Date: Sun, 11 May 2025 16:16:30 +0200 Subject: [PATCH] added logic and styles for custom dropdown components based on
element --- the_works/static/the_works.css | 38 ++++++++ the_works/static/the_works.js | 168 ++++++++++++++++++++++++++++----- 2 files changed, 181 insertions(+), 25 deletions(-) diff --git a/the_works/static/the_works.css b/the_works/static/the_works.css index c0cb44e..c039c46 100644 --- a/the_works/static/the_works.css +++ b/the_works/static/the_works.css @@ -89,3 +89,41 @@ label:has([type="checkbox"]) { #navbar li[aria-current=page]>details>summary>a { text-decoration: underline; } + +/* correct inconsistent margins when using
in a form */ +label > :where(div) { + margin-top: calc(var(--pico-spacing) * 0.25); +} + +label:has(details.dropdown) { + margin-bottom: calc(var(--pico-spacing) * 1.25); +} + +/* filter out elements by search */ +.filtered-out { + display: none; +} + +/* placeholder for */ +summary:empty::before { + content: attr(data-default); +} + +/* badge styles for the selected items of a dropdown select field built with
and */ +.dropdown-badge { + background-color: var(--pico-primary); + color: var(--pico-background-color); + font-size: .8em; + padding: calc(var(--pico-form-element-spacing-vertical) * .5) calc(var(--pico-form-element-spacing-horizontal) * .5); + margin: auto calc(var(--pico-spacing) * .25); + cursor: default; +} + +.dropdown-badge-close { + cursor: pointer; +} + +form:not([novalidate]) input:user-valid[type="search"] { + border-color: inherit; + background-image: none; +} \ No newline at end of file diff --git a/the_works/static/the_works.js b/the_works/static/the_works.js index ad2bd90..191d075 100644 --- a/the_works/static/the_works.js +++ b/the_works/static/the_works.js @@ -1,4 +1,7 @@ +// 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, @@ -13,34 +16,149 @@ function initDataTable(table_id) { } // create "New"-element and append it to the
containing the DataTables search field -function initCreateButton(opts) { - let a = document.createElement("a"); - a.id = "create-button"; - a.setAttribute("title", opts.title); - a.setAttribute("role", "button"); - a.setAttribute("href", opts.href || "#"); - a.innerHTML = "Neu …"; - document.getElementById(`${opts.table_id.slice(0, 1) == "#" ? opts.table_id.slice(1) : opts.table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(a); +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); } -function showDialog(opts) { +// 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 ( opts.url_id && opts.form_action.includes("update") ) { - opts.form_action = opts.form_action.slice(0, opts.form_action.lastIndexOf("/") + 1) + opts.url_id; + if ( form_action.includes("update") && url_id) { + form_action = form_action.slice(0, form_action.lastIndexOf("/") + 1) + url_id; } - // if input id or input value is not an array, make it a single-element array - opts.input_id = Array.isArray(opts.input_id) ? opts.input_id : [ opts.input_id ]; - if ( opts.input_value ) { - opts.input_value = Array.isArray(opts.input_value) ? opts.input_value : [ opts.input_value ]; - } else { - opts.input_value = new Array(opts.input_id.length).fill(""); + // 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] || ""; } - - console.log(`id[] is ${JSON.stringify(opts.input_id)}, value[] is ${JSON.stringify(opts.input_value)}`); - document.getElementById("dialog-heading").textContent = opts.heading; - for (var i = 0; i < opts.input_id.length; i++ ) { - document.getElementById(opts.input_id[i]).value = opts.input_value[i] || ""; - } - document.getElementById("form_submit").formAction = opts.form_action; - document.getElementById(opts.modal_id).showModal(); + // raise modal + 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(); + } + } +} + +// 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 genre 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 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(); }