Skip to content

Commit 2a18c82

Browse files
committed
feat: enhance category selection and filtering with multiple selection
1 parent bd71301 commit 2a18c82

File tree

1 file changed

+93
-33
lines changed

1 file changed

+93
-33
lines changed

src/resources/projects/website/listing/quarto-listing.js

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
const kProgressiveAttr = "data-src";
22
let categoriesLoaded = false;
3+
let selectedCategories = new Set();
4+
const kDefaultCategory = ""; // Default category "" means all posts selected
35

46
window.quartoListingCategory = (category) => {
57
// category is URI encoded in EJS template for UTF-8 support
68
category = decodeURIComponent(atob(category));
79
if (categoriesLoaded) {
810
activateCategory(category);
9-
setCategoryHash(category);
11+
setCategoryHash();
1012
}
1113
};
1214

@@ -15,11 +17,19 @@ window["quarto-listing-loaded"] = () => {
1517
const hash = getHash();
1618

1719
if (hash) {
18-
// If there is a category, switch to that
19-
if (hash.category) {
20-
// category hash are URI encoded so we need to decode it before processing
21-
// so that we can match it with the category element processed in JS
22-
activateCategory(decodeURIComponent(hash.category));
20+
// If there are categories, switch to those
21+
if (hash.categories) {
22+
const cats = hash.categories.split(",");
23+
for (const cat of cats) {
24+
if (cat) selectedCategories.add(decodeURIComponent(cat));
25+
}
26+
updateCategoryUI();
27+
filterListingCategories();
28+
} else {
29+
// No categories in hash, use default
30+
selectedCategories.add(kDefaultCategory);
31+
updateCategoryUI();
32+
filterListingCategories();
2333
}
2434
// Paginate a specific listing
2535
const listingIds = Object.keys(window["quarto-listings"]);
@@ -29,6 +39,11 @@ window["quarto-listing-loaded"] = () => {
2939
showPage(listingId, page);
3040
}
3141
}
42+
} else {
43+
// No hash at all, use default category
44+
selectedCategories.add(kDefaultCategory);
45+
updateCategoryUI();
46+
filterListingCategories();
3247
}
3348

3449
const listingIds = Object.keys(window["quarto-listings"]);
@@ -66,9 +81,14 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
6681
const category = decodeURIComponent(
6782
atob(categoryEl.getAttribute("data-category"))
6883
);
69-
categoryEl.onclick = () => {
84+
categoryEl.onclick = (e) => {
85+
// Allow holding Ctrl/Cmd key for multiple selection
86+
// Clear other selections if not using Ctrl/Cmd
87+
if (!e.ctrlKey && !e.metaKey) {
88+
selectedCategories.clear();
89+
}
7090
activateCategory(category);
71-
setCategoryHash(category);
91+
setCategoryHash();
7292
};
7393
}
7494

@@ -79,11 +99,29 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
7999
);
80100
for (const categoryTitleEl of categoryTitleEls) {
81101
categoryTitleEl.onclick = () => {
82-
activateCategory("");
83-
setCategoryHash("");
102+
selectedCategories.clear();
103+
updateCategoryUI();
104+
setCategoryHash();
105+
filterListingCategories();
84106
};
85107
}
86108

109+
// Process any existing hash for multiple categories
110+
const hash = getHash();
111+
if (hash && hash.categories) {
112+
const cats = hash.categories.split(",");
113+
for (const cat of cats) {
114+
if (cat) selectedCategories.add(decodeURIComponent(cat));
115+
}
116+
updateCategoryUI();
117+
filterListingCategories();
118+
} else {
119+
// No hash at all, use default category
120+
selectedCategories.add(kDefaultCategory);
121+
updateCategoryUI();
122+
filterListingCategories();
123+
}
124+
87125
categoriesLoaded = true;
88126
});
89127

@@ -101,8 +139,15 @@ function toggleNoMatchingMessage(list) {
101139
}
102140
}
103141

104-
function setCategoryHash(category) {
105-
setHash({ category });
142+
function setCategoryHash() {
143+
if (selectedCategories.size === 0) {
144+
setHash({});
145+
} else {
146+
const categoriesStr = Array.from(selectedCategories)
147+
.map((cat) => encodeURIComponent(cat))
148+
.join(",");
149+
setHash({ category: categoriesStr });
150+
}
106151
}
107152

108153
function setPageHash(listingId, page) {
@@ -205,45 +250,60 @@ function showPage(listingId, page) {
205250
}
206251

207252
function activateCategory(category) {
208-
// Deactivate existing categories
209-
const activeEls = window.document.querySelectorAll(
210-
".quarto-listing-category .category.active"
211-
);
212-
for (const activeEl of activeEls) {
213-
activeEl.classList.remove("active");
253+
if (selectedCategories.has(category)) {
254+
selectedCategories.delete(category);
255+
} else {
256+
selectedCategories.add(category);
214257
}
258+
updateCategoryUI();
259+
filterListingCategories();
260+
}
215261

216-
// Activate this category
217-
const categoryEl = window.document.querySelector(
218-
`.quarto-listing-category .category[data-category='${btoa(
219-
encodeURIComponent(category)
220-
)}']`
262+
function updateCategoryUI() {
263+
// Deactivate all categories first
264+
const activeEls = window.document.querySelectorAll(
265+
".quarto-listing-category .category"
221266
);
222-
if (categoryEl) {
223-
categoryEl.classList.add("active");
267+
for (const activeEls of activeEls) {
268+
activeEls.classList.remove("active");
224269
}
225270

226-
// Filter the listings to this category
227-
filterListingCategory(category);
271+
// Activate selected categories
272+
for (const category of selectedCategories) {
273+
const categoryEl = window.document.querySelector(
274+
`.quarto-listing-category .category[data-category='${btoa(
275+
encodeURIComponent(category)
276+
)}']`
277+
);
278+
if (categoryEl) {
279+
categoryEl.classList.add("active");
280+
}
281+
}
228282
}
229283

230-
function filterListingCategory(category) {
284+
function filterListingCategories() {
231285
const listingIds = Object.keys(window["quarto-listings"]);
232286
for (const listingId of listingIds) {
233287
const list = window["quarto-listings"][listingId];
234288
if (list) {
235-
if (category === "") {
236-
// resets the filter
289+
if (selectedCategories.size === 0 ||
290+
(selectedCategories.size === 1 && selectedCategories.has(kDefaultCategory))) {
291+
// Reset the filter when no categories selected or only default category
237292
list.filter();
238293
} else {
239-
// filter to this category
294+
// Filter to selected categories, but ignore kDefaultCategory if other categories selected
295+
const effectiveCategories = new Set(selectedCategories);
296+
if (effectiveCategories.size > 1) {
297+
effectiveCategories.delete(kDefaultCategory);
298+
}
299+
240300
list.filter(function (item) {
241301
const itemValues = item.values();
242302
if (itemValues.categories !== null) {
243-
const categories = decodeURIComponent(
303+
const itemCategories = decodeURIComponent(
244304
atob(itemValues.categories)
245305
).split(",");
246-
return categories.includes(category);
306+
return itemCategories.some(category => effectiveCategories.has(category));
247307
} else {
248308
return false;
249309
}

0 commit comments

Comments
 (0)