11const kProgressiveAttr = "data-src" ;
22let categoriesLoaded = false ;
3+ let selectedCategories = new Set ( ) ;
4+ const kDefaultCategory = "" ; // Default category "" means all posts selected
35
46window . 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
108153function setPageHash ( listingId , page ) {
@@ -205,45 +250,60 @@ function showPage(listingId, page) {
205250}
206251
207252function 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