Skip to content
136 changes: 97 additions & 39 deletions src/resources/projects/website/listing/quarto-listing.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
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);
}
selectedCategories.clear();
selectedCategories.add(category);

updateCategory();
setCategoryHash();
};

window["quarto-listing-loaded"] = () => {
// Process any existing hash
const hash = getHash();

if (hash) {
// If there is a category, switch to that
// If there are categories, switch to those
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));
const cats = hash.category.split(",");
for (const cat of cats) {
if (cat) selectedCategories.add(decodeURIComponent(cat));
}
updateCategory();
} else {
// No categories in hash, use default
selectedCategories.add(kDefaultCategory);
updateCategory();
}
// Paginate a specific listing
const listingIds = Object.keys(window["quarto-listings"]);
Expand All @@ -29,6 +37,10 @@ window["quarto-listing-loaded"] = () => {
showPage(listingId, page);
}
}
} else {
// No hash at all, use default category
selectedCategories.add(kDefaultCategory);
updateCategory();
}

const listingIds = Object.keys(window["quarto-listings"]);
Expand Down Expand Up @@ -66,9 +78,25 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
const category = decodeURIComponent(
atob(categoryEl.getAttribute("data-category"))
);
categoryEl.onclick = () => {
activateCategory(category);
setCategoryHash(category);
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();
}
Comment on lines +82 to +86
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that is a good idea as it makes the multiple selection feature "hidden".
I thought this was less disruptive than checkboxes everywhere.
This was the only idea I came up with to keep previous visual appearance.


// If this would deselect the last category, ensure default category remains selected
if (selectedCategories.has(category)) {
selectedCategories.delete(category);
if (selectedCategories.size === 1) {
selectedCategories.add(kDefaultCategory);
}
} else {
selectedCategories.add(category);
}

updateCategory();
setCategoryHash();
};
}

Expand All @@ -79,12 +107,25 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
);
for (const categoryTitleEl of categoryTitleEls) {
categoryTitleEl.onclick = () => {
activateCategory("");
setCategoryHash("");
selectedCategories.clear();
updateCategory();
setCategoryHash();
};
}

categoriesLoaded = true;
// Process any existing hash for multiple categories
const hash = getHash();
if (hash && hash.category) {
const cats = hash.category.split(",");
for (const cat of cats) {
if (cat) selectedCategories.add(decodeURIComponent(cat));
}
updateCategory();
} else {
// No hash at all, use default category
selectedCategories.add(kDefaultCategory);
updateCategory();
}
});

function toggleNoMatchingMessage(list) {
Expand All @@ -101,8 +142,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) {
Expand Down Expand Up @@ -204,46 +252,56 @@ 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");
}
function updateCategory() {
updateCategoryUI();
filterListingCategory();
}

// Activate this category
const categoryEl = window.document.querySelector(
`.quarto-listing-category .category[data-category='${btoa(
encodeURIComponent(category)
)}']`
function updateCategoryUI() {
// Deactivate all categories first
const categoryEls = window.document.querySelectorAll(
".quarto-listing-category .category"
);
if (categoryEl) {
categoryEl.classList.add("active");
for (const categoryEl of categoryEls) {
categoryEl.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 filterListingCategory() {
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.has(kDefaultCategory)) {
effectiveCategories.delete(kDefaultCategory);
}

list.filter(function (item) {
const itemValues = item.values();
if (itemValues.categories !== null) {
const categories = decodeURIComponent(
atob(itemValues.categories)
).split(",");
return categories.includes(category);
return categories.some(category => effectiveCategories.has(category));
} else {
return false;
}
Expand Down
Loading