From 2a18c826a314e1a03073a0f72991d035e357a124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 2 May 2025 19:04:39 +0200 Subject: [PATCH 1/7] feat: enhance category selection and filtering with multiple selection --- .../website/listing/quarto-listing.js | 126 +++++++++++++----- 1 file changed, 93 insertions(+), 33 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index e9a07b2ea39..5dd8b77e3bc 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -1,12 +1,14 @@ const kProgressiveAttr = "data-src"; let categoriesLoaded = false; +let selectedCategories = new Set(); +const kDefaultCategory = ""; // Default category "" means all posts selected window.quartoListingCategory = (category) => { // category is URI encoded in EJS template for UTF-8 support category = decodeURIComponent(atob(category)); if (categoriesLoaded) { activateCategory(category); - setCategoryHash(category); + setCategoryHash(); } }; @@ -15,11 +17,19 @@ window["quarto-listing-loaded"] = () => { const hash = getHash(); if (hash) { - // If there is a category, switch to that - if (hash.category) { - // category hash are URI encoded so we need to decode it before processing - // so that we can match it with the category element processed in JS - activateCategory(decodeURIComponent(hash.category)); + // If there are categories, switch to those + if (hash.categories) { + const cats = hash.categories.split(","); + for (const cat of cats) { + if (cat) selectedCategories.add(decodeURIComponent(cat)); + } + updateCategoryUI(); + filterListingCategories(); + } else { + // No categories in hash, use default + selectedCategories.add(kDefaultCategory); + updateCategoryUI(); + filterListingCategories(); } // Paginate a specific listing const listingIds = Object.keys(window["quarto-listings"]); @@ -29,6 +39,11 @@ window["quarto-listing-loaded"] = () => { showPage(listingId, page); } } + } else { + // No hash at all, use default category + selectedCategories.add(kDefaultCategory); + updateCategoryUI(); + filterListingCategories(); } const listingIds = Object.keys(window["quarto-listings"]); @@ -66,9 +81,14 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { const category = decodeURIComponent( atob(categoryEl.getAttribute("data-category")) ); - categoryEl.onclick = () => { + categoryEl.onclick = (e) => { + // Allow holding Ctrl/Cmd key for multiple selection + // Clear other selections if not using Ctrl/Cmd + if (!e.ctrlKey && !e.metaKey) { + selectedCategories.clear(); + } activateCategory(category); - setCategoryHash(category); + setCategoryHash(); }; } @@ -79,11 +99,29 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { ); for (const categoryTitleEl of categoryTitleEls) { categoryTitleEl.onclick = () => { - activateCategory(""); - setCategoryHash(""); + selectedCategories.clear(); + updateCategoryUI(); + setCategoryHash(); + filterListingCategories(); }; } + // Process any existing hash for multiple categories + const hash = getHash(); + if (hash && hash.categories) { + const cats = hash.categories.split(","); + for (const cat of cats) { + if (cat) selectedCategories.add(decodeURIComponent(cat)); + } + updateCategoryUI(); + filterListingCategories(); + } else { + // No hash at all, use default category + selectedCategories.add(kDefaultCategory); + updateCategoryUI(); + filterListingCategories(); + } + categoriesLoaded = true; }); @@ -101,8 +139,15 @@ function toggleNoMatchingMessage(list) { } } -function setCategoryHash(category) { - setHash({ category }); +function setCategoryHash() { + if (selectedCategories.size === 0) { + setHash({}); + } else { + const categoriesStr = Array.from(selectedCategories) + .map((cat) => encodeURIComponent(cat)) + .join(","); + setHash({ category: categoriesStr }); + } } function setPageHash(listingId, page) { @@ -205,45 +250,60 @@ function showPage(listingId, page) { } function activateCategory(category) { - // Deactivate existing categories - const activeEls = window.document.querySelectorAll( - ".quarto-listing-category .category.active" - ); - for (const activeEl of activeEls) { - activeEl.classList.remove("active"); + if (selectedCategories.has(category)) { + selectedCategories.delete(category); + } else { + selectedCategories.add(category); } + updateCategoryUI(); + filterListingCategories(); +} - // Activate this category - const categoryEl = window.document.querySelector( - `.quarto-listing-category .category[data-category='${btoa( - encodeURIComponent(category) - )}']` +function updateCategoryUI() { + // Deactivate all categories first + const activeEls = window.document.querySelectorAll( + ".quarto-listing-category .category" ); - if (categoryEl) { - categoryEl.classList.add("active"); + for (const activeEls of activeEls) { + activeEls.classList.remove("active"); } - // Filter the listings to this category - filterListingCategory(category); + // Activate selected categories + for (const category of selectedCategories) { + const categoryEl = window.document.querySelector( + `.quarto-listing-category .category[data-category='${btoa( + encodeURIComponent(category) + )}']` + ); + if (categoryEl) { + categoryEl.classList.add("active"); + } + } } -function filterListingCategory(category) { +function filterListingCategories() { const listingIds = Object.keys(window["quarto-listings"]); for (const listingId of listingIds) { const list = window["quarto-listings"][listingId]; if (list) { - if (category === "") { - // resets the filter + if (selectedCategories.size === 0 || + (selectedCategories.size === 1 && selectedCategories.has(kDefaultCategory))) { + // Reset the filter when no categories selected or only default category list.filter(); } else { - // filter to this category + // Filter to selected categories, but ignore kDefaultCategory if other categories selected + const effectiveCategories = new Set(selectedCategories); + if (effectiveCategories.size > 1) { + effectiveCategories.delete(kDefaultCategory); + } + list.filter(function (item) { const itemValues = item.values(); if (itemValues.categories !== null) { - const categories = decodeURIComponent( + const itemCategories = decodeURIComponent( atob(itemValues.categories) ).split(","); - return categories.includes(category); + return itemCategories.some(category => effectiveCategories.has(category)); } else { return false; } From 08d00e068ed7f3faf445cd5939a8eba70fa37922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 2 May 2025 19:20:24 +0200 Subject: [PATCH 2/7] revert: keep original names --- .../website/listing/quarto-listing.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index 5dd8b77e3bc..4ce84abe0fe 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -24,12 +24,12 @@ window["quarto-listing-loaded"] = () => { if (cat) selectedCategories.add(decodeURIComponent(cat)); } updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } else { // No categories in hash, use default selectedCategories.add(kDefaultCategory); updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } // Paginate a specific listing const listingIds = Object.keys(window["quarto-listings"]); @@ -43,7 +43,7 @@ window["quarto-listing-loaded"] = () => { // No hash at all, use default category selectedCategories.add(kDefaultCategory); updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } const listingIds = Object.keys(window["quarto-listings"]); @@ -102,7 +102,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { selectedCategories.clear(); updateCategoryUI(); setCategoryHash(); - filterListingCategories(); + filterListingCategory(); }; } @@ -114,12 +114,12 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { if (cat) selectedCategories.add(decodeURIComponent(cat)); } updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } else { // No hash at all, use default category selectedCategories.add(kDefaultCategory); updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } categoriesLoaded = true; @@ -256,7 +256,7 @@ function activateCategory(category) { selectedCategories.add(category); } updateCategoryUI(); - filterListingCategories(); + filterListingCategory(); } function updateCategoryUI() { @@ -281,7 +281,7 @@ function updateCategoryUI() { } } -function filterListingCategories() { +function filterListingCategory() { const listingIds = Object.keys(window["quarto-listings"]); for (const listingId of listingIds) { const list = window["quarto-listings"][listingId]; @@ -300,10 +300,10 @@ function filterListingCategories() { list.filter(function (item) { const itemValues = item.values(); if (itemValues.categories !== null) { - const itemCategories = decodeURIComponent( + const categories = decodeURIComponent( atob(itemValues.categories) ).split(","); - return itemCategories.some(category => effectiveCategories.has(category)); + return categories.some(category => effectiveCategories.has(category)); } else { return false; } From 87248446a6236696b488eb25b9a328e9f42a35a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 2 May 2025 19:54:09 +0200 Subject: [PATCH 3/7] fix: logic for default category and keep "category" as URL hash key --- .../website/listing/quarto-listing.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index 4ce84abe0fe..1c7f34071c7 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -18,8 +18,8 @@ window["quarto-listing-loaded"] = () => { if (hash) { // If there are categories, switch to those - if (hash.categories) { - const cats = hash.categories.split(","); + if (hash.category) { + const cats = hash.category.split(","); for (const cat of cats) { if (cat) selectedCategories.add(decodeURIComponent(cat)); } @@ -87,6 +87,12 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { if (!e.ctrlKey && !e.metaKey) { selectedCategories.clear(); } + + // If this would deselect the last category, ensure default category remains selected + if (selectedCategories.has(category) && selectedCategories.size === 1) { + selectedCategories.add(kDefaultCategory); + } + activateCategory(category); setCategoryHash(); }; @@ -108,8 +114,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { // Process any existing hash for multiple categories const hash = getHash(); - if (hash && hash.categories) { - const cats = hash.categories.split(","); + if (hash && hash.category) { + const cats = hash.category.split(","); for (const cat of cats) { if (cat) selectedCategories.add(decodeURIComponent(cat)); } @@ -261,11 +267,11 @@ function activateCategory(category) { function updateCategoryUI() { // Deactivate all categories first - const activeEls = window.document.querySelectorAll( + const categoryEls = window.document.querySelectorAll( ".quarto-listing-category .category" ); - for (const activeEls of activeEls) { - activeEls.classList.remove("active"); + for (const categoryEl of categoryEls) { + categoryEl.classList.remove("active"); } // Activate selected categories @@ -293,7 +299,7 @@ function filterListingCategory() { } else { // Filter to selected categories, but ignore kDefaultCategory if other categories selected const effectiveCategories = new Set(selectedCategories); - if (effectiveCategories.size > 1) { + if (effectiveCategories.size > 1 && effectiveCategories.has(kDefaultCategory)) { effectiveCategories.delete(kDefaultCategory); } From 94fbe36bf36fb7f5823d4fd10c902c845225e66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 2 May 2025 20:32:47 +0200 Subject: [PATCH 4/7] fix: clear and add post category to hash --- src/resources/projects/website/listing/quarto-listing.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index 1c7f34071c7..ca06d55a54b 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -6,10 +6,11 @@ const kDefaultCategory = ""; // Default category "" means all posts selected window.quartoListingCategory = (category) => { // category is URI encoded in EJS template for UTF-8 support category = decodeURIComponent(atob(category)); - if (categoriesLoaded) { - activateCategory(category); - setCategoryHash(); - } + selectedCategories.clear(); + selectedCategories.add(category); + updateCategoryUI(); + filterListingCategory(); + setCategoryHash(); }; window["quarto-listing-loaded"] = () => { From a42adc83164339a741b021ad429ddd86aa73f3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 7 May 2025 21:18:20 +0200 Subject: [PATCH 5/7] refactor: improve logic and dry code a bit --- .../website/listing/quarto-listing.js | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index ca06d55a54b..c46a4b1c13d 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -8,9 +8,9 @@ window.quartoListingCategory = (category) => { category = decodeURIComponent(atob(category)); selectedCategories.clear(); selectedCategories.add(category); - updateCategoryUI(); - filterListingCategory(); + setCategoryHash(); + updateCategory(); }; window["quarto-listing-loaded"] = () => { @@ -24,13 +24,11 @@ window["quarto-listing-loaded"] = () => { for (const cat of cats) { if (cat) selectedCategories.add(decodeURIComponent(cat)); } - updateCategoryUI(); - filterListingCategory(); + updateCategory(); } else { // No categories in hash, use default selectedCategories.add(kDefaultCategory); - updateCategoryUI(); - filterListingCategory(); + updateCategory(); } // Paginate a specific listing const listingIds = Object.keys(window["quarto-listings"]); @@ -43,8 +41,7 @@ window["quarto-listing-loaded"] = () => { } else { // No hash at all, use default category selectedCategories.add(kDefaultCategory); - updateCategoryUI(); - filterListingCategory(); + updateCategory(); } const listingIds = Object.keys(window["quarto-listings"]); @@ -90,12 +87,17 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { } // If this would deselect the last category, ensure default category remains selected - if (selectedCategories.has(category) && selectedCategories.size === 1) { - selectedCategories.add(kDefaultCategory); + if (selectedCategories.has(category)) { + selectedCategories.delete(category); + if (selectedCategories.size === 1) { + selectedCategories.add(kDefaultCategory); + } + } else { + selectedCategories.add(category); } - activateCategory(category); setCategoryHash(); + updateCategory(); }; } @@ -107,9 +109,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { for (const categoryTitleEl of categoryTitleEls) { categoryTitleEl.onclick = () => { selectedCategories.clear(); - updateCategoryUI(); setCategoryHash(); - filterListingCategory(); + updateCategory(); }; } @@ -120,13 +121,11 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { for (const cat of cats) { if (cat) selectedCategories.add(decodeURIComponent(cat)); } - updateCategoryUI(); - filterListingCategory(); + updateCategory(); } else { // No hash at all, use default category selectedCategories.add(kDefaultCategory); - updateCategoryUI(); - filterListingCategory(); + updateCategory(); } categoriesLoaded = true; @@ -256,12 +255,7 @@ function showPage(listingId, page) { } } -function activateCategory(category) { - if (selectedCategories.has(category)) { - selectedCategories.delete(category); - } else { - selectedCategories.add(category); - } +function updateCategory() { updateCategoryUI(); filterListingCategory(); } From 150bee68d6b295899737dcc51e84f7cd45d2b39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 7 May 2025 21:19:50 +0200 Subject: [PATCH 6/7] refactor: hash after update --- src/resources/projects/website/listing/quarto-listing.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index c46a4b1c13d..ebd71fb6610 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -9,8 +9,8 @@ window.quartoListingCategory = (category) => { selectedCategories.clear(); selectedCategories.add(category); - setCategoryHash(); updateCategory(); + setCategoryHash(); }; window["quarto-listing-loaded"] = () => { @@ -96,8 +96,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { selectedCategories.add(category); } - setCategoryHash(); updateCategory(); + setCategoryHash(); }; } @@ -109,8 +109,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { for (const categoryTitleEl of categoryTitleEls) { categoryTitleEl.onclick = () => { selectedCategories.clear(); - setCategoryHash(); updateCategory(); + setCategoryHash(); }; } From 0cb3c76cc689b0d4c519b393efc9dc31c7c9a7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 8 May 2025 11:28:15 +0200 Subject: [PATCH 7/7] chore: remove no longer used variable --- src/resources/projects/website/listing/quarto-listing.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js index ebd71fb6610..5c826814d57 100644 --- a/src/resources/projects/website/listing/quarto-listing.js +++ b/src/resources/projects/website/listing/quarto-listing.js @@ -1,5 +1,4 @@ const kProgressiveAttr = "data-src"; -let categoriesLoaded = false; let selectedCategories = new Set(); const kDefaultCategory = ""; // Default category "" means all posts selected @@ -127,8 +126,6 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { selectedCategories.add(kDefaultCategory); updateCategory(); } - - categoriesLoaded = true; }); function toggleNoMatchingMessage(list) {