const html = { getToggleFiltersButton: () => document.querySelector("#toggle-filters"), getFiltersForm: () => document.querySelector("#filters"), getJobsTableBody: () => document.querySelector(".jobs > tbody"), getFieldsetCheckbox: (id) => document.querySelector(`fieldset > legend input[id=${id}][type=checkbox]`), getFieldsetsCheckboxes: () => document.querySelectorAll(`fieldset > legend input[id][type=checkbox]`), getFieldsetOptions: (name, opts) => { let query = `input[name=${name}][type=checkbox]`; if (opts.isChecked != null && opts.isChecked) { query = `${query}:checked` } if (opts.isChecked != null && !opts.isChecked) { query = `${query}:not(:checked)` } return document.querySelectorAll(query); }, getTagsSearchInput: () => document.querySelector("#tags-search"), getCountriesSearchInput: () => document.querySelector("#countries-search"), filterOptions: (query, selectorName, datasetName) => { let rows = document.querySelectorAll(selectorName); rows.forEach(row => { if (!fuzzysearch(query, row.dataset[datasetName]) && query) { row.style.display = "none"; } else { row.style.display = ""; } }) } }; const FETCH_TIMEOUT = 400; // Source: https://github.com/bevacqua/fuzzysearch function fuzzysearch (needle, haystack) { var hlen = haystack.length; var nlen = needle.length; if (nlen > hlen) { return false; } if (nlen === hlen) { return needle === haystack; } outer: for (var i = 0, j = 0; i < nlen; i++) { var nch = needle.charCodeAt(i); while (j < hlen) { if (haystack.charCodeAt(j++) === nch) { continue outer; } } return false; } return true; } // On page load let submitTicker = null; html.getToggleFiltersButton()?.addEventListener("click", (e) => { let filtersForm = html.getFiltersForm(); if (filtersForm.style.display == "block") { filtersForm.style.display = "none"; } else { filtersForm.style.display = "block"; } }); html.getFiltersForm()?.addEventListener("input", (e) => { if (submitTicker != null) { clearTimeout(submitTicker); } let isTextInput = e.target.type == "text" || e.target.type == "search" || e.target.type == "number"; let delay = isTextInput ? FETCH_TIMEOUT : 0; let form = html.getFiltersForm(); // If it's a fieldset checkbox, then we toggle all of the checkboxes in the // fieldset whose `name` matches the ID. if (e.target.type == "checkbox" && e.target.id != "") { let checkboxes = document.querySelectorAll(`input[name=${e.target.id}][type=checkbox]`); checkboxes.forEach((el) => el.checked = e.target.checked); } else if (e.target.type == "checkbox" && e.target.name != "") { let fieldsetCheckbox = html.getFieldsetCheckbox(e.target.name); let uncheckedBoxes = html.getFieldsetOptions(e.target.name, {"isChecked": false}); let checkedBoxes = html.getFieldsetOptions(e.target.name, {"isChecked": true}); fieldsetCheckbox.indeterminate = uncheckedBoxes.length >= 1 && checkedBoxes.length >= 1; fieldsetCheckbox.checked = checkedBoxes.length >= 1 && uncheckedBoxes.length == 0; } submitTicker = setTimeout(() => submitForm(toFormData(form), form.action), delay); }); function toFormData(form) { const formData = new FormData(form); let minSalary = formData.get("min_salary"); if (minSalary && parseInt(minSalary) <= 0) { formData.delete("min_salary"); } formData.entries().forEach(([k, v]) => { if (v == "") { formData.delete(k); } }); return formData; } async function submitForm(formData, action) { console.log(formData); const queryParams = new URLSearchParams(formData); // Push new query parameters to history const url = new URL(location); url.search = ""; queryParams.entries().forEach(([k,v]) => { url.searchParams.append(k, v); }); history.pushState({}, "", url); // Dispatch request and replace content const res = await fetch(action + '?' + queryParams.toString(), { method: "GET", headers: { 'Hx-Request': 'true' } }); let htmlRes = await res.text(); html.getJobsTableBody().innerHTML = htmlRes; } let fieldsetCheckboxes = html.getFieldsetsCheckboxes(); fieldsetCheckboxes.forEach(el => { let uncheckedBoxes = html.getFieldsetOptions(el.id, {"isChecked": false}); let checkedBoxes = html.getFieldsetOptions(el.id, {"isChecked": true}); el.indeterminate = uncheckedBoxes.length >= 1 && checkedBoxes.length >= 1 }); html.getTagsSearchInput().addEventListener("input", (e) => { html.filterOptions(html.getTagsSearchInput().value?.toLowerCase(), ".tag[data-tag-name]", "tagName"); }); html.getCountriesSearchInput().addEventListener("input", (e) => { html.filterOptions(html.getCountriesSearchInput().value?.toLowerCase(), ".country[data-country-name]", "countryName"); });