Skip to content
126 changes: 93 additions & 33 deletions src/resources/projects/website/listing/quarto-listing.js
Original file line number Diff line number Diff line change
@@ -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();
}
};

Expand All @@ -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"]);
Expand All @@ -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"]);
Expand Down Expand Up @@ -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();
}
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.

activateCategory(category);
setCategoryHash(category);
setCategoryHash();
};
}

Expand All @@ -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;
});

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
Loading