added logic and styles for custom dropdown components based on <details> element

This commit is contained in:
eclipse 2025-05-11 16:16:30 +02:00
parent aeb63cd482
commit dcafefadf6
2 changed files with 181 additions and 25 deletions

View File

@ -89,3 +89,41 @@ label:has([type="checkbox"]) {
#navbar li[aria-current=page]>details>summary>a {
text-decoration: underline;
}
/* correct inconsistent margins when using <details> 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> */
summary:empty::before {
content: attr(data-default);
}
/* badge styles for the selected items of a dropdown select field built with <details> and <summary> */
.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;
}

View File

@ -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 <div> 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 <a> 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 = "&#10006;";
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();
}