From a3c29ebd0ebbb1b60cf3d1075cf599adf48e19e9 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 10:26:26 +0100 Subject: Dynamically update URL with search query. --- pydis_site/static/js/resources/resources.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 508849e1..c4d01f9d 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -51,6 +51,13 @@ function noFilters() { function deserializeURLParams() { let searchParams = new window.URLSearchParams(window.location.search); + // Add the search query to the search bar. + if (searchParams.has("search")) { + let searchQuery = searchParams.get("search"); + console.log("Adding query to search box! Query is ${searchQuery}"); + $("#resource-search input").val(searchQuery); + } + // Work through the parameters and add them to the filter object $.each(Object.keys(activeFilters), function(_, filterType) { let paramFilterContent = searchParams.get(filterType); @@ -62,11 +69,13 @@ function deserializeURLParams() { // 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. + // Catch special cases. 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"; + + // If the filter is valid, mirror it to the UI. } 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}']`); @@ -93,8 +102,10 @@ function deserializeURLParams() { /* 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()) { + let searchQuery = $("#resource-search input").val(); + + // If there's no active filtering parameters, we can return early. + if (noFilters() && searchQuery.length === 0) { window.history.replaceState(null, document.title, './'); return; } @@ -107,6 +118,11 @@ function updateURL() { } }); + // Add the search query, if necessary. + if (searchQuery.length > 0) { + searchParams.set("search", searchQuery); + } + // Now update the URL window.history.replaceState(null, document.title, `?${searchParams.toString()}`); } -- cgit v1.2.3 From 364060841c39c12973e6edd1854d251d5d8011c9 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 10:26:51 +0100 Subject: Trigger a UI update when typing into search. --- pydis_site/static/js/resources/resources.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index c4d01f9d..348571e6 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -246,6 +246,11 @@ document.addEventListener("DOMContentLoaded", function () { setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10); } + // When you type into the search bar, trigger an UI update. + $("#resource-search input").on("input", function() { + updateUI(); + }); + // 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)); -- cgit v1.2.3 From 9db38103343a662248aae9d4a134b93a2113b72f Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 12:05:52 +0100 Subject: Implement fuzzy search. This implements the core logics for filtering by search. It uses fuzzysort to match the search query to the name of the resource. This name is set in the yaml for each resource. --- pydis_site/static/js/resources/resources.js | 39 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 348571e6..79c33cc7 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -8,6 +8,12 @@ var activeFilters = { difficulty: [] }; +// Options for fuzzysort +const fuzzysortOptions = { + allowTypo: true, // Allow our users to make typos + threshold: -10000, // The threshold for the fuzziness. Adjust for precision. +}; + /* Add a filter, and update the UI */ function addFilter(filterName, filterItem) { var filterIndex = activeFilters[filterName].indexOf(filterItem); @@ -54,7 +60,6 @@ function deserializeURLParams() { // Add the search query to the search bar. if (searchParams.has("search")) { let searchQuery = searchParams.get("search"); - console.log("Adding query to search box! Query is ${searchQuery}"); $("#resource-search input").val(searchQuery); } @@ -127,6 +132,17 @@ function updateURL() { window.history.replaceState(null, document.title, `?${searchParams.toString()}`); } +/* Apply search terms */ +function filterBySearch(resourceItems) { + let searchQuery = $("#resource-search input").val(); + + resourceItems.filter(function() { + // Run a fuzzy search over the item. Does the search query match? + let name = $(this).attr("name"); + return Boolean(fuzzysort.single(searchQuery, name, fuzzysortOptions)); + }).show(); +} + /* Update the resources to match 'active_filters' */ function updateUI() { let resources = $('.resource-box'); @@ -134,19 +150,28 @@ function updateUI() { let resourceTags = $('.resource-tag'); let noTagsSelected = $(".no-tags-selected.tag"); let closeFiltersButton = $(".close-filters-button"); + let searchQuery = $("#resource-search input").val(); // Update the URL to match the new filters. updateURL(); // If there's nothing in the filters, we can return early. if (noFilters()) { - resources.show(); + // If we have a searchQuery, we need to run all resources through a search. + if (searchQuery.length > 0) { + resources.hide(); + filterBySearch(resources); + } else { + 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 @@ -176,7 +201,7 @@ function updateUI() { // Otherwise, hide everything and then filter the resources to decide what to show. let hasMatches = false; resources.hide(); - resources.filter(function() { + let filteredResources = resources.filter(function() { let validation = { topics: false, type: false, @@ -208,8 +233,14 @@ function updateUI() { } else { return false; } - }).show(); + }); + // Run the items we've found through the search filter, if necessary. + if (searchQuery.length > 0) { + filterBySearch(filteredResources); + } else { + filteredResources.show(); + } // If there are no matches, show the no matches message if (!hasMatches) { -- cgit v1.2.3 From 6addd63c2f5ea34382d3561073945a9ae6f2ea12 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 13:18:48 +0100 Subject: Add a filter tag when searching. --- pydis_site/static/css/resources/resources.css | 8 ++++++++ pydis_site/static/js/resources/resources.js | 17 +++++++++++++++-- pydis_site/templates/resources/resources.html | 10 ++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css index f46803e2..6f97422b 100644 --- a/pydis_site/static/css/resources/resources.css +++ b/pydis_site/static/css/resources/resources.css @@ -91,6 +91,14 @@ display: block; display: flex !important; } +/* By default, we hide the search tag. We'll add it only when there's a search happening. */ +.tag.search-query { + display: none; +} +.tag.search-query .inner { + padding: 0; +} + /* Disable clicking on the checkbox itself. */ /* Instead, we want to let the anchor tag handle clicks. */ .filter-checkbox { diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 79c33cc7..713a7402 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -136,10 +136,19 @@ function updateURL() { function filterBySearch(resourceItems) { let searchQuery = $("#resource-search input").val(); + /* Show and update the tag if there's a search query */ + if (searchQuery) { + let tag = $(".tag.search-query"); + let tagText = $(".tag.search-query span"); + tagText.text(`Search: ${searchQuery}`); + tag.show(); + } + resourceItems.filter(function() { // Run a fuzzy search over the item. Does the search query match? let name = $(this).attr("name"); - return Boolean(fuzzysort.single(searchQuery, name, fuzzysortOptions)); + let result = fuzzysort.single(searchQuery, name, fuzzysortOptions); + return Boolean(result) && result.score > fuzzysortOptions.threshold; }).show(); } @@ -151,6 +160,7 @@ function updateUI() { let noTagsSelected = $(".no-tags-selected.tag"); let closeFiltersButton = $(".close-filters-button"); let searchQuery = $("#resource-search input").val(); + let searchTag = $(".tag.search-query"); // Update the URL to match the new filters. updateURL(); @@ -160,13 +170,15 @@ function updateUI() { // If we have a searchQuery, we need to run all resources through a search. if (searchQuery.length > 0) { resources.hide(); + noTagsSelected.hide(); filterBySearch(resources); } else { resources.show(); + noTagsSelected.show(); + $(".tag.search-query").hide(); } filterTags.hide(); - noTagsSelected.show(); closeFiltersButton.hide(); resourceTags.removeClass("active"); $(`.filter-checkbox:checked`).prop("checked", false); @@ -240,6 +252,7 @@ function updateUI() { filterBySearch(filteredResources); } else { filteredResources.show(); + searchTag.hide(); } // If there are no matches, show the no matches message diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html index 1f16e5ec..19750612 100644 --- a/pydis_site/templates/resources/resources.html +++ b/pydis_site/templates/resources/resources.html @@ -44,11 +44,17 @@
{# A filter tag for when there are no filters active #} - - + + No filters selected + {# A filter tag for search queries #} + + + Search: ... + + {% for filter_name, filter_data in filters.items %} {% for filter_item in filter_data.filters %} {% if filter_name == "Difficulty" %} -- cgit v1.2.3 From 1ea2cbb5d7793b3e6c023a65572357630f33342c Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 14:07:18 +0100 Subject: Search the resource description too. --- pydis_site/static/js/resources/resources.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 713a7402..bbf07c94 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -10,8 +10,9 @@ var activeFilters = { // Options for fuzzysort const fuzzysortOptions = { - allowTypo: true, // Allow our users to make typos - threshold: -10000, // The threshold for the fuzziness. Adjust for precision. + allowTypo: true, // Allow our users to make typos + titleThreshold: -10000, // The threshold for the fuzziness on title matches. Closer to 0 is stricter. + descriptionThreshold: -500, // The threshold for the fuzziness on description matches. }; /* Add a filter, and update the UI */ @@ -145,10 +146,18 @@ function filterBySearch(resourceItems) { } resourceItems.filter(function() { - // Run a fuzzy search over the item. Does the search query match? - let name = $(this).attr("name"); - let result = fuzzysort.single(searchQuery, name, fuzzysortOptions); - return Boolean(result) && result.score > fuzzysortOptions.threshold; + // Get the resource title and description + let title = $(this).attr("name"); + let description = $(this).find("p").text(); + + // Run a fuzzy search. Does the title or description match the query? + let titleMatch = fuzzysort.single(searchQuery, title, fuzzysortOptions); + titleMatch = Boolean(titleMatch) && titleMatch.score > fuzzysortOptions.titleThreshold; + + let descriptionMatch = fuzzysort.single(searchQuery, description, fuzzysortOptions); + descriptionMatch = Boolean(descriptionMatch) && descriptionMatch.score > fuzzysortOptions.descriptionThreshold; + + return titleMatch || descriptionMatch; }).show(); } -- cgit v1.2.3 From b4fc433e22ec7192ac5dbe43aa6f483574ac1726 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 14:12:53 +0100 Subject: Fix duck pond not showing for search results of 0. --- pydis_site/static/js/resources/resources.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index bbf07c94..b74c28ec 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -220,7 +220,6 @@ function updateUI() { } // Otherwise, hide everything and then filter the resources to decide what to show. - let hasMatches = false; resources.hide(); let filteredResources = resources.filter(function() { let validation = { @@ -249,7 +248,6 @@ function updateUI() { // If validation passes, show the resource. if (Object.values(validation).every(Boolean)) { - hasMatches = true; return true; } else { return false; @@ -265,7 +263,8 @@ function updateUI() { } // If there are no matches, show the no matches message - if (!hasMatches) { + let visibleResources = Boolean($(".resource-box:visible").length); + if (!visibleResources) { $(".no-resources-found").show(); } else { $(".no-resources-found").hide(); -- cgit v1.2.3 From 0b8da4d9f15f5f596512aa71365cd7e959846a87 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 14:17:25 +0100 Subject: Make the remove all tags affect search, too. --- pydis_site/static/js/resources/resources.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index b74c28ec..113ce502 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -32,6 +32,7 @@ function removeAllFilters() { "payment-tiers": [], difficulty: [] }; + $("#resource-search input").val(""); updateUI(); } @@ -62,6 +63,7 @@ function deserializeURLParams() { if (searchParams.has("search")) { let searchQuery = searchParams.get("search"); $("#resource-search input").val(searchQuery); + $(".close-filters-button").show(); } // Work through the parameters and add them to the filter object @@ -143,6 +145,7 @@ function filterBySearch(resourceItems) { let tagText = $(".tag.search-query span"); tagText.text(`Search: ${searchQuery}`); tag.show(); + $(".close-filters-button").show(); } resourceItems.filter(function() { @@ -184,11 +187,11 @@ function updateUI() { } else { resources.show(); noTagsSelected.show(); + closeFiltersButton.hide(); $(".tag.search-query").hide(); } filterTags.hide(); - closeFiltersButton.hide(); resourceTags.removeClass("active"); $(`.filter-checkbox:checked`).prop("checked", false); $(".no-resources-found").hide(); -- cgit v1.2.3 From 33e8f0cb477547bbef7564a83353d2379caa9e0e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 16:53:32 +0100 Subject: Best practice: Switch name to data-resource-name. --- pydis_site/static/js/fuzzysort/LICENSE.md | 2 +- pydis_site/static/js/fuzzysort/fuzzysort.js | 2 +- pydis_site/static/js/resources/resources.js | 2 +- pydis_site/templates/resources/resource_box.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/fuzzysort/LICENSE.md b/pydis_site/static/js/fuzzysort/LICENSE.md index 736006f1..a3b9d9d7 100644 --- a/pydis_site/static/js/fuzzysort/LICENSE.md +++ b/pydis_site/static/js/fuzzysort/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/pydis_site/static/js/fuzzysort/fuzzysort.js b/pydis_site/static/js/fuzzysort/fuzzysort.js index 88cfe570..ba01ae63 100644 --- a/pydis_site/static/js/fuzzysort/fuzzysort.js +++ b/pydis_site/static/js/fuzzysort/fuzzysort.js @@ -633,4 +633,4 @@ return fuzzysortNew() // TODO: (like sublime) backslash === forwardslash // TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b // TODO: (scoring) garbage in targets that allows most searches to strict match need a penality -// TODO: (performance) idk if allowTypo is optimized \ No newline at end of file +// TODO: (performance) idk if allowTypo is optimized diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 113ce502..6e4a09ba 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -150,7 +150,7 @@ function filterBySearch(resourceItems) { resourceItems.filter(function() { // Get the resource title and description - let title = $(this).attr("name"); + let title = $(this).attr("data-resource-name"); let description = $(this).find("p").text(); // Run a fuzzy search. Does the title or description match the query? diff --git a/pydis_site/templates/resources/resource_box.html b/pydis_site/templates/resources/resource_box.html index 5189bb3e..5ca46296 100644 --- a/pydis_site/templates/resources/resource_box.html +++ b/pydis_site/templates/resources/resource_box.html @@ -2,7 +2,7 @@ {% load to_kebabcase %} {% load get_category_icon %} -
+
{% if 'title_url' in resource %} {% include "resources/resource_box_header.html" %} -- cgit v1.2.3 From fbea3ccd4e7bf90c6efa67f4a5b98148eb4af23f Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 13 Feb 2022 20:03:46 +0100 Subject: Edge cases: Show duckies when no visible resources --- pydis_site/static/js/resources/resources.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'pydis_site/static/js/resources/resources.js') diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js index 6e4a09ba..d6cc8128 100644 --- a/pydis_site/static/js/resources/resources.js +++ b/pydis_site/static/js/resources/resources.js @@ -108,6 +108,17 @@ function deserializeURLParams() { }); } +/* Show or hide the duckies, depending on whether or not there are any resources visible. */ +function updateDuckies() { + let visibleResources = Boolean($(".resource-box:visible").length); + if (!visibleResources) { + $(".no-resources-found").show(); + } else { + $(".no-resources-found").hide(); + } +} + + /* Update the URL with new parameters */ function updateURL() { let searchQuery = $("#resource-search input").val(); @@ -194,7 +205,7 @@ function updateUI() { filterTags.hide(); resourceTags.removeClass("active"); $(`.filter-checkbox:checked`).prop("checked", false); - $(".no-resources-found").hide(); + updateDuckies(); return; } else { @@ -265,13 +276,8 @@ function updateUI() { searchTag.hide(); } - // If there are no matches, show the no matches message - let visibleResources = Boolean($(".resource-box:visible").length); - if (!visibleResources) { - $(".no-resources-found").show(); - } else { - $(".no-resources-found").hide(); - } + // Gotta update those duckies! + updateDuckies(); } // Executed when the page has finished loading. -- cgit v1.2.3