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 { #navbar li[aria-current=page]>details>summary>a {
text-decoration: underline; 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) { function initDataTable(table_id) {
// add # to id if not there already
table_id = table_id.slice(0, 1) == "#" ? table_id : "#" + table_id
// initialize table // initialize table
let table = new DataTable(table_id, { let table = new DataTable(table_id, {
paging: false, paging: false,
@ -13,34 +16,149 @@ function initDataTable(table_id) {
} }
// create "New"-element and append it to the <div> containing the DataTables search field // create "New"-element and append it to the <div> containing the DataTables search field
function initCreateButton(opts) { function initCreateButton(table_id, title, href=null) {
let a = document.createElement("a"); // build button element
a.id = "create-button"; let button = document.createElement("button");
a.setAttribute("title", opts.title); button.id = "create-button";
a.setAttribute("role", "button"); button.setAttribute("title", title);
a.setAttribute("href", opts.href || "#"); button.textContent = "Neu …";
a.innerHTML = "Neu …"; // wrap button inside an <a> if href is given
document.getElementById(`${opts.table_id.slice(0, 1) == "#" ? opts.table_id.slice(1) : opts.table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(a); 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 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") ) { if ( form_action.includes("update") && url_id) {
opts.form_action = opts.form_action.slice(0, opts.form_action.lastIndexOf("/") + 1) + opts.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 // set modal attributes
opts.input_id = Array.isArray(opts.input_id) ? opts.input_id : [ opts.input_id ]; document.getElementById("dialog-heading").textContent = heading;
if ( opts.input_value ) { document.getElementById("form_submit").formAction = form_action;
opts.input_value = Array.isArray(opts.input_value) ? opts.input_value : [ opts.input_value ]; for (var i = 0; i < input_ids.length; i++ ) {
} else { document.getElementById(input_ids[i]).value = input_values[i] || "";
opts.input_value = new Array(opts.input_id.length).fill("");
} }
// raise modal
console.log(`id[] is ${JSON.stringify(opts.input_id)}, value[] is ${JSON.stringify(opts.input_value)}`); document.getElementById(modal_id).showModal();
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] || ""; // 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") {
document.getElementById("form_submit").formAction = opts.form_action; let t = document.getElementById(tag_id);
document.getElementById(opts.modal_id).showModal(); 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();
} }