let page = "Padliography"; let endpoint = `https://pzwiki.wdka.nl/mw-mediadesign/api.php?action=parse&format=json&origin=*&page=${page}&prop=text`; const container = document.getElementById("wiki-contents"); const filtersContainer = document.getElementById("filters-container"); const activeAllButton = document.getElementById("active-all"); // We use a set and not an array in order to avoid repetition for the categories let filters = new Set(); // Variables for the filter system let allActive = true; let tagList; // Global variable for the generated table let table; // Request the content to the Wiki API and parse it as a JSON file fetch(endpoint) .then((response) => response.json()) .then((data) => { // TODO: custom class in the table to call the element // Create a new element to parse the response from the wiki and get an HTML table element out of it let section = document.createElement("div"); section.innerHTML = data.parse.text["*"]; // Find the table with the padliography custom class (remember to set it up in the wiki table) let wikiTable = section.getElementsByClassName("padliography")[0]; // Create a new table using the data in the Wiki table createTable(wikiTable); // Fill the filter section with all the tags gathered in the filters Set // We don't need any argument because the filters set is defined as global createFilters(); // Initially show all the categories showAllTags(); // Sort by date, last first (oke srry for this, the sorting function is a toggle atm) sortTable(headers["Date"]); sortTable(headers["Date"]); }); // --- // Contents Generation // --- function createTable(data) { table = document.createElement("table"); table.appendChild(createHeaders()); // Traverse the rows collection as an array [...data.rows].forEach((row, index) => { // Avoid first row that contains column titles if (index > 0) { // Create a row element let tableRow = document.createElement("tr"); // Create a list of tags from the categories cell let categoryTags = row.cells[3].innerText.split(",").map((category) => category.trim()); // Set the categories as classes for future filtering categoryTags.forEach((tag) => tableRow.classList.add(tag)); // --- --- --- // --- Beginning of the row // Columns order could be modified simply by reordering the next 4 sections // Create a cell with date let date = document.createElement("td"); date.classList.add("date"); date.innerHTML = createDate(row.cells[4].innerText); tableRow.appendChild(date); // Create a cell with title + link to the pad let title = document.createElement("td"); title.classList.add("title"); title.appendChild(createTitleLink(row)); tableRow.appendChild(title); // Create a cell with categories let categories = document.createElement("td"); categories.classList.add("categories"); categories.appendChild(createTags(categoryTags)); tableRow.appendChild(categories); // Create a cell with the overview let overview = document.createElement("td"); overview.classList.add("overview"); overview.innerHTML = row.cells[2].innerText; tableRow.appendChild(overview); // --- End of the row // --- --- --- // Insert the row in the table table.appendChild(tableRow); } }); // Insert the table in the container container.appendChild(table); } let headers = {}; function createHeaders() { let firstRow = document.createElement("tr"); firstRow.classList.add("header"); let titles = ["Date", "Title", "Categories", "Overview"]; titles.forEach((title) => { let th = document.createElement("th"); th.innerHTML = title; // Add sorting callback for Date and Title // Do we need to sort also the categories? im not sure! if (title === "Date" || title === "Title") th.addEventListener("click", () => sortTable(th)); firstRow.appendChild(th); // Add header to the headers object in order to use the sort later headers[title] = th; }); return firstRow; } function createTitleLink(row) { // Take the first cell of the wiki (link) let url = row.cells[0].innerText; // Take the second cell of the wiki (title) let title = row.cells[1].innerText; // Create a link element let link = document.createElement("a"); link.setAttribute("target", "_blank"); // TODO: stretched link for making all the row clickabe? link.classList.add("stretched-link"); // Use the link as href link.href = url; // Use the title as text link.innerHTML = title; return link; } function createDate(string) { let parsed = string.replaceAll("/", "."); let date = new Date(string); if (date) { const options = { year: "numeric", month: "short", day: "numeric" }; parsed = date.toLocaleDateString("en-EN", options); if (parsed === "Invalid Date") parsed = "???"; } return parsed; } // Take a list of categories and create an unordered list with them // + store every category to the filters Set function createTags(categories) { // Create a list to contain all the tags let tagsContainer = document.createElement("ul"); // For each category create a li element categories .sort((a, b) => a.localeCompare(b)) .forEach((item) => { let tag = document.createElement("li"); tag.classList.add("tag"); tag.innerHTML = item; tagsContainer.appendChild(tag); // add the category to the filters Set to use it later filters.add(item); }); return tagsContainer; } // --- // Filtering // --- // Parse the filters Set to create a list of interactive list elements function createFilters() { Array.from(filters) .sort((a, b) => a.localeCompare(b)) .forEach((item) => { let tag = document.createElement("li"); // Store the filter value in the markup and use it for filtering later tag.setAttribute("data-tag", item); // Tab index is for accessibility via keyboard tag.setAttribute("tabindex", 0); tag.setAttribute("role", "button"); tag.classList.add("tag"); tag.innerHTML = item; filtersContainer.appendChild(tag); }); // Build a list of the tag for managing the filter tagList = document.querySelectorAll("[data-tag]"); } // Manage all the combinations when a filter is clicked or selected function conditionalToggle(element) { // If all the filters are active disable them all except the one selected if (allActive) { tagList.forEach((tag) => { filters.delete(tag.getAttribute("data-tag")); tag.classList.remove("active"); tag.setAttribute("aria-expanded", "false"); }); activeAllButton.classList.remove("active"); allActive = false; } // If the filter is active turn it off, else the contrary if (element.classList.contains("active")) { element.classList.remove("active"); element.setAttribute("aria-expanded", "false"); filters.delete(element.getAttribute("data-tag")); } else { element.classList.add("active"); element.setAttribute("aria-expanded", "true"); filters.add(element.getAttribute("data-tag")); } // Check if every filter is active or not // use it to apply the correct style when all the filters are active allActive = tagList.length == filters.size; if (allActive) { activeAllButton.classList.add("active"); activeAllButton.setAttribute("aria-expanded", "true"); } // If there are no filters active enable them all if (filters.size === 0) { showAllTags(); } } // Display the rows that have their categories in the filters Set function showSelectedRows() { [...table.rows].forEach((row) => { row.classList.remove("active"); }); filters.forEach((filter) => { let selectedRows = [...table.rows].filter((el) => { filter = filter.replaceAll(" ", "-"); return el.classList.contains(filter); }); selectedRows.forEach((row) => row.classList.add("active")); }); allActiveCheck(); } function showAllTags() { tagList.forEach((tag) => { filters.add(tag.getAttribute("data-tag")); tag.classList.add("active"); tag.setAttribute("aria-expanded", "true"); }); allActive = true; showSelectedRows(); } function allActiveCheck() { if (tagList.length === filters.size) { allActive = true; tagList.forEach((tag) => tag.classList.add("all")); filtersContainer.firstElementChild.classList.add("active"); } else { allActive = false; tagList.forEach((tag) => tag.classList.remove("all")); filtersContainer.firstElementChild.classList.remove("active"); } } // --- // Sorting table // kindly adapted from https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript // TODO: Sorting UI // --- const getCellValue = (tr, idx) => { if (tr.children[idx].classList.contains("date")) return new Date(tr.children[idx].innerText); else return tr.children[idx].innerText || tr.children[idx].textContent; }; // wow this is really obscure wtf not happy with it at all const comparer = (idx, asc) => (a, b) => ((v1, v2) => v1 !== "" && v2 !== "" && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2))( getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx) ); // The function is added as callback in the createHeaders function const sortTable = (th) => { const table = th.closest("table"); Array.from(table.querySelectorAll("tr:nth-child(n+2)")) .sort(comparer(Array.from(th.parentNode.children).indexOf(th), (this.asc = !this.asc))) .forEach((tr) => table.appendChild(tr)); }; // --- // Event Listener // --- filtersContainer.addEventListener("click", (e) => { if (e.target.tagName === "LI") { conditionalToggle(e.target); showSelectedRows(); } }); activeAllButton.addEventListener("click", (e) => { activeAllButton.classList.add("active"); activeAllButton.setAttribute("aria-expanded", "true"); showAllTags(); }); document.addEventListener("keydown", (event) => { if (event.isComposing || event.keyCode === 229) { return; } if (event.keyCode === 32) { if (document.activeElement.tagName === "LI") { conditionalToggle(document.activeElement); showSelectedRows(); } } });