diff options
author | 2022-02-02 00:15:44 +0100 | |
---|---|---|
committer | 2022-02-02 00:15:44 +0100 | |
commit | 09fb2a4df59be324fc41176227ae3c684e2f6add (patch) | |
tree | 9d5baa069f91419e3189115d143129b6dc4b8e31 /pydis_site/static/js | |
parent | Merge pull request #649 from python-discord/update-pyfakefs (diff) | |
parent | Duck pond removed when removing all filters. (diff) |
Merge pull request #582 from python-discord/swfarnsworth/smarter-resources/merge-with-main
Smarter Resources
Diffstat (limited to 'pydis_site/static/js')
-rw-r--r-- | pydis_site/static/js/collapsibles.js | 67 | ||||
-rw-r--r-- | pydis_site/static/js/content/page.js | 13 | ||||
-rw-r--r-- | pydis_site/static/js/resources/resources.js | 285 |
3 files changed, 352 insertions, 13 deletions
diff --git a/pydis_site/static/js/collapsibles.js b/pydis_site/static/js/collapsibles.js new file mode 100644 index 00000000..1df0b9fe --- /dev/null +++ b/pydis_site/static/js/collapsibles.js @@ -0,0 +1,67 @@ +/* +A utility for creating simple collapsible cards. + +To see this in action, go to /resources or /pages/guides/pydis-guides/contributing/bot/ + +// HOW TO USE THIS // +First, import this file and the corresponding css file into your template. + + <link rel="stylesheet" href="{% static "css/collapsibles.css" %}"> + <script defer src="{% static "js/collapsibles.js" %}"></script> + +Next, you'll need some HTML that these scripts can interact with. + +<div class="card"> + <button type="button" class="card-header collapsible"> + <span class="card-header-title subtitle is-6 my-2 ml-2">Your headline</span> + <span class="card-header-icon"> + <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> + </span> + </button> + <div class="collapsible-content collapsed"> + <div class="card-content"> + You can put anything you want here. Lists, more divs, flexboxes, images, whatever. + </div> + </div> +</div> + +That's it! Collapsing stuff should now work. + */ + +document.addEventListener("DOMContentLoaded", () => { + const contentContainers = document.getElementsByClassName("collapsible-content"); + for (const container of contentContainers) { + // Close any collapsibles that are marked as initially collapsed + if (container.classList.contains("collapsed")) { + container.style.maxHeight = "0px"; + // Set maxHeight to the size of the container on all other containers. + } else { + container.style.maxHeight = container.scrollHeight + "px"; + } + } + + // Listen for click events, and collapse or explode + const headers = document.getElementsByClassName("collapsible"); + for (const header of headers) { + const content = header.nextElementSibling; + const icon = header.querySelector(".card-header-icon i"); + + // Any collapsibles that are not initially collapsed needs an icon switch. + if (!content.classList.contains("collapsed")) { + icon.classList.remove("fas", "fa-angle-down"); + icon.classList.add("far", "fa-window-minimize"); + } + + header.addEventListener("click", () => { + if (content.style.maxHeight !== "0px"){ + content.style.maxHeight = "0px"; + icon.classList.remove("far", "fa-window-minimize"); + icon.classList.add("fas", "fa-angle-down"); + } else { + content.style.maxHeight = content.scrollHeight + "px"; + icon.classList.remove("fas", "fa-angle-down"); + icon.classList.add("far", "fa-window-minimize"); + } + }); + } +}); diff --git a/pydis_site/static/js/content/page.js b/pydis_site/static/js/content/page.js deleted file mode 100644 index 366a033c..00000000 --- a/pydis_site/static/js/content/page.js +++ /dev/null @@ -1,13 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const headers = document.getElementsByClassName("collapsible"); - for (const header of headers) { - header.addEventListener("click", () => { - var content = header.nextElementSibling; - if (content.style.maxHeight){ - content.style.maxHeight = null; - } else { - content.style.maxHeight = content.scrollHeight + "px"; - } - }); - } -}); diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js new file mode 100644 index 00000000..508849e1 --- /dev/null +++ b/pydis_site/static/js/resources/resources.js @@ -0,0 +1,285 @@ +"use strict"; + +// Filters that are currently selected +var activeFilters = { + topics: [], + type: [], + "payment-tiers": [], + difficulty: [] +}; + +/* Add a filter, and update the UI */ +function addFilter(filterName, filterItem) { + var filterIndex = activeFilters[filterName].indexOf(filterItem); + if (filterIndex === -1) { + activeFilters[filterName].push(filterItem); + } + updateUI(); +} + +/* Remove all filters, and update the UI */ +function removeAllFilters() { + activeFilters = { + topics: [], + type: [], + "payment-tiers": [], + difficulty: [] + }; + updateUI(); +} + +/* Remove a filter, and update the UI */ +function removeFilter(filterName, filterItem) { + var filterIndex = activeFilters[filterName].indexOf(filterItem); + if (filterIndex !== -1) { + activeFilters[filterName].splice(filterIndex, 1); + } + updateUI(); +} + +/* Check if there are no filters */ +function noFilters() { + return ( + activeFilters.topics.length === 0 && + activeFilters.type.length === 0 && + activeFilters["payment-tiers"].length === 0 && + activeFilters.difficulty.length === 0 + ); +} + +/* Get the params out of the URL and use them. This is run when the page loads. */ +function deserializeURLParams() { + let searchParams = new window.URLSearchParams(window.location.search); + + // Work through the parameters and add them to the filter object + $.each(Object.keys(activeFilters), function(_, filterType) { + let paramFilterContent = searchParams.get(filterType); + + if (paramFilterContent !== null) { + // We use split here because we always want an array, not a string. + let paramFilterArray = paramFilterContent.split(","); + + // Update the corresponding filter UI, so it reflects the internal state. + let filterAdded = false; + $(paramFilterArray).each(function(_, filter) { + // Make sure the filter is valid before we do anything. + if (String(filter) === "rickroll" && filterType === "type") { + window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; + } else if (String(filter) === "sneakers" && filterType === "topics") { + window.location.href = "https://www.youtube.com/watch?v=NNZscmNE9QI"; + } else if (validFilters.hasOwnProperty(filterType) && validFilters[filterType].includes(String(filter))) { + let checkbox = $(`.filter-checkbox[data-filter-name='${filterType}'][data-filter-item='${filter}']`); + let filterTag = $(`.filter-box-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`); + let resourceTags = $(`.resource-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`); + checkbox.prop("checked", true); + filterTag.show(); + resourceTags.addClass("active"); + activeFilters[filterType].push(filter); + filterAdded = true; + } + }); + + // Ditch all the params from the URL, and recalculate the URL params + updateURL(); + + // If we've added a filter, hide stuff + if (filterAdded) { + $(".no-tags-selected.tag").hide(); + $(".close-filters-button").show(); + } + } + }); +} + +/* Update the URL with new parameters */ +function updateURL() { + // If there's nothing in the filters, we don't want anything in the URL. + if (noFilters()) { + window.history.replaceState(null, document.title, './'); + return; + } + + // Iterate through and get rid of empty ones + let searchParams = new URLSearchParams(activeFilters); + $.each(activeFilters, function(filterType, filters) { + if (filters.length === 0) { + searchParams.delete(filterType); + } + }); + + // Now update the URL + window.history.replaceState(null, document.title, `?${searchParams.toString()}`); +} + +/* Update the resources to match 'active_filters' */ +function updateUI() { + let resources = $('.resource-box'); + let filterTags = $('.filter-box-tag'); + let resourceTags = $('.resource-tag'); + let noTagsSelected = $(".no-tags-selected.tag"); + let closeFiltersButton = $(".close-filters-button"); + + // Update the URL to match the new filters. + updateURL(); + + // If there's nothing in the filters, we can return early. + if (noFilters()) { + resources.show(); + filterTags.hide(); + noTagsSelected.show(); + closeFiltersButton.hide(); + resourceTags.removeClass("active"); + $(`.filter-checkbox:checked`).prop("checked", false); + $(".no-resources-found").hide(); + return; + } else { + // Hide everything + $('.filter-box-tag').hide(); + $('.resource-tag').removeClass("active"); + noTagsSelected.show(); + closeFiltersButton.hide(); + + // Now conditionally show the stuff we want + $.each(activeFilters, function(filterType, filters) { + $.each(filters, function(index, filter) { + // Show a corresponding filter box tag + $(`.filter-box-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).show(); + + // Make corresponding resource tags active + $(`.resource-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).addClass("active"); + + // Hide the "No filters selected" tag. + noTagsSelected.hide(); + + // Show the close filters button + closeFiltersButton.show(); + }); + }); + } + + // Otherwise, hide everything and then filter the resources to decide what to show. + let hasMatches = false; + resources.hide(); + resources.filter(function() { + let validation = { + topics: false, + type: false, + 'payment-tiers': false, + difficulty: false + }; + let resourceBox = $(this); + + // Validate the filters + $.each(activeFilters, function(filterType, filters) { + // If the filter list is empty, this passes validation. + if (filters.length === 0) { + validation[filterType] = true; + return; + } + + // Otherwise, we need to check if one of the classes exist. + $.each(filters, function(index, filter) { + if (resourceBox.hasClass(`${filterType}-${filter}`)) { + validation[filterType] = true; + } + }); + }); + + // If validation passes, show the resource. + if (Object.values(validation).every(Boolean)) { + hasMatches = true; + return true; + } else { + return false; + } + }).show(); + + + // If there are no matches, show the no matches message + if (!hasMatches) { + $(".no-resources-found").show(); + } else { + $(".no-resources-found").hide(); + } +} + +// Executed when the page has finished loading. +document.addEventListener("DOMContentLoaded", function () { + /* Check if the user has navigated to one of the old resource pages, + like pydis.com/resources/communities. In this case, we'll rewrite + the URL before we do anything else. */ + let resourceTypeInput = $("#resource-type-input").val(); + if (resourceTypeInput !== "None") { + window.history.replaceState(null, document.title, `../?type=${resourceTypeInput}`); + } + + // Update the filters on page load to reflect URL parameters. + $('.filter-box-tag').hide(); + deserializeURLParams(); + updateUI(); + + // If this is a mobile device, collapse all the categories to win back some screen real estate. + if (screen.width < 480) { + let categoryHeaders = $(".filter-category-header .collapsible-content"); + let icons = $('.filter-category-header button .card-header-icon i'); + categoryHeaders.addClass("no-transition collapsed"); + icons.removeClass(["far", "fa-window-minimize"]); + icons.addClass(["fas", "fa-angle-down"]); + + // Wait 10ms before removing this class, or else the transition will animate due to a race condition. + setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10); + } + + // If you click on the div surrounding the filter checkbox, it clicks the corresponding checkbox. + $('.filter-panel').on("click",function(event) { + let hitsCheckbox = Boolean(String(event.target)); + + if (!hitsCheckbox) { + let checkbox = $(this).find(".filter-checkbox"); + checkbox.prop("checked", !checkbox.prop("checked")); + checkbox.trigger("change"); + } + }); + + // If you click on one of the tags in the filter box, it unchecks the corresponding checkbox. + $('.filter-box-tag').on("click", function() { + let filterItem = this.dataset.filterItem; + let filterName = this.dataset.filterName; + let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`); + + removeFilter(filterName, filterItem); + checkbox.prop("checked", false); + }); + + // If you click on one of the tags in the resource cards, it clicks the corresponding checkbox. + $('.resource-tag').on("click", function() { + let filterItem = this.dataset.filterItem; + let filterName = this.dataset.filterName; + let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`); + + if (!$(this).hasClass("active")) { + addFilter(filterName, filterItem); + checkbox.prop("checked", true); + } else { + removeFilter(filterName, filterItem); + checkbox.prop("checked", false); + } + }); + + // When you click the little gray x, remove all filters. + $(".close-filters-button").on("click", function() { + removeAllFilters(); + }); + + // When checkboxes are toggled, trigger a filter update. + $('.filter-checkbox').on("change", function (event) { + let filterItem = this.dataset.filterItem; + let filterName = this.dataset.filterName; + + if (this.checked && !activeFilters[filterName].includes(filterItem)) { + addFilter(filterName, filterItem); + } else if (!this.checked && activeFilters[filterName].includes(filterItem)) { + removeFilter(filterName, filterItem); + } + }); +}); |