PageSourceSearch

https://bouacida.fr/site_libs/quarto-listing/quarto-listing.js

js bouacida.fr collected 2026-09-24 10:35:46 UTC 7,111 bytes, 254 lines download raw bytes

1const kProgressiveAttr = "data-src";
2let categoriesLoaded = false;
3
4window.quartoListingCategory = (category) => {
5  // category is URI encoded in EJS template for UTF-8 support
6  category = decodeURIComponent(atob(category));
7  if (categoriesLoaded) {
8    activateCategory(category);
9    setCategoryHash(category);
10  }
11};
12
13window["quarto-listing-loaded"] = () => {
14  // Process any existing hash
15  const hash = getHash();
16
17  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));
23    }
24    // Paginate a specific listing
25    const listingIds = Object.keys(window["quarto-listings"]);
26    for (const listingId of listingIds) {
27      const page = hash[getListingPageKey(listingId)];
28      if (page) {
29        showPage(listingId, page);
30      }
31    }
32  }
33
34  const listingIds = Object.keys(window["quarto-listings"]);
35  for (const listingId of listingIds) {
36    // The actual list
37    const list = window["quarto-listings"][listingId];
38
39    // Update the handlers for pagination events
40    refreshPaginationHandlers(listingId);
41
42    // Render any visible items that need it
43    renderVisibleProgressiveImages(list);
44
45    // Whenever the list is updated, we also need to
46    // attach handlers to the new pagination elements
47    // and refresh any newly visible items.
48    list.on("updated", function () {
49      renderVisibleProgressiveImages(list);
50      setTimeout(() => refreshPaginationHandlers(listingId));
51
52      // Show or hide the no matching message
53      toggleNoMatchingMessage(list);
54    });
55  }
56};
57
58window.document.addEventListener("DOMContentLoaded", function (_event) {
59  // Attach click handlers to categories
60  const categoryEls = window.document.querySelectorAll(
61    ".quarto-listing-category .category"
62  );
63
64  for (const categoryEl of categoryEls) {
65    // category needs to support non ASCII characters
66    const category = decodeURIComponent(
67      atob(categoryEl.getAttribute("data-category"))
68    );
69    categoryEl.onclick = () => {
70      activateCategory(category);
71      setCategoryHash(category);
72    };
73  }
74
75  // Attach a click handler to the category title
76  // (there should be only one, but since it is a class name, handle N)
77  const categoryTitleEls = window.document.querySelectorAll(
78    ".quarto-listing-category-title"
79  );
80  for (const categoryTitleEl of categoryTitleEls) {
81    categoryTitleEl.onclick = () => {
82      activateCategory("");
83      setCategoryHash("");
84    };
85  }
86
87  categoriesLoaded = true;
88});
89
90function toggleNoMatchingMessage(list) {
91  const selector = `#${list.listContainer.id} .listing-no-matching`;
92  const noMatchingEl = window.document.querySelector(selector);
93  if (noMatchingEl) {
94    if (list.visibleItems.length === 0) {
95      noMatchingEl.classList.remove("d-none");
96    } else {
97      if (!noMatchingEl.classList.contains("d-none")) {
98        noMatchingEl.classList.add("d-none");
99      }
100    }
101  }
102}
103
104function setCategoryHash(category) {
105  setHash({ category });
106}
107
108function setPageHash(listingId, page) {
109  const currentHash = getHash() || {};
110  currentHash[getListingPageKey(listingId)] = page;
111  setHash(currentHash);
112}
113
114function getListingPageKey(listingId) {
115  return `${listingId}-page`;
116}
117
118function refreshPaginationHandlers(listingId) {
119  const listingEl = window.document.getElementById(listingId);
120  const paginationEls = listingEl.querySelectorAll(
121    ".pagination li.page-item:not(.disabled) .page.page-link"
122  );
123  for (const paginationEl of paginationEls) {
124    paginationEl.onclick = (sender) => {
125      setPageHash(listingId, sender.target.getAttribute("data-i"));
126      showPage(listingId, sender.target.getAttribute("data-i"));
127      return false;
128    };
129  }
130}
131
132function renderVisibleProgressiveImages(list) {
133  // Run through the visible items and render any progressive images
134  for (const item of list.visibleItems) {
135    const itemEl = item.elm;
136    if (itemEl) {
137      const progressiveImgs = itemEl.querySelectorAll(
138        `img[${kProgressiveAttr}]`
139      );
140      for (const progressiveImg of progressiveImgs) {
141        const srcValue = progressiveImg.getAttribute(kProgressiveAttr);
142        if (srcValue) {
143          progressiveImg.setAttribute("src", srcValue);
144        }
145        progressiveImg.removeAttribute(kProgressiveAttr);
146      }
147    }
148  }
149}
150
151function getHash() {
152  // Hashes are of the form
153  // #name:value|name1:value1|name2:value2
154  const currentUrl = new URL(window.location);
155  const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined;
156  return parseHash(hashRaw);
157}
158
159const kAnd = "&";
160const kEquals = "=";
161
162function parseHash(hash) {
163  if (!hash) {
164    return undefined;
165  }
166  const hasValuesStrs = hash.split(kAnd);
167  const hashValues = hasValuesStrs
168    .map((hashValueStr) => {
169      const vals = hashValueStr.split(kEquals);
170      if (vals.length === 2) {
171        return { name: vals[0], value: vals[1] };
172      } else {
173        return undefined;
174      }
175    })
176    .filter((value) => {
177      return value !== undefined;
178    });
179
180  const hashObj = {};
181  hashValues.forEach((hashValue) => {
182    hashObj[hashValue.name] = decodeURIComponent(hashValue.value);
183  });
184  return hashObj;
185}
186
187function makeHash(obj) {
188  return Object.keys(obj)
189    .map((key) => {
190      return `${key}${kEquals}${obj[key]}`;
191    })
192    .join(kAnd);
193}
194
195function setHash(obj) {
196  const hash = makeHash(obj);
197  window.history.pushState(null, null, `#${hash}`);
198}
199
200function showPage(listingId, page) {
201  const list = window["quarto-listings"][listingId];
202  if (list) {
203    list.show((page - 1) * list.page + 1, list.page);
204  }
205}
206
207function 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");
214  }
215
216  // Activate this category
217  const categoryEl = window.document.querySelector(
218    `.quarto-listing-category .category[data-category='${btoa(
219      encodeURIComponent(category)
220    )}']`
221  );
222  if (categoryEl) {
223    categoryEl.classList.add("active");
224  }
225
226  // Filter the listings to this category
227  filterListingCategory(category);
228}
229
230function filterListingCategory(category) {
231  const listingIds = Object.keys(window["quarto-listings"]);
232  for (const listingId of listingIds) {
233    const list = window["quarto-listings"][listingId];
234    if (list) {
235      if (category === "") {
236        // resets the filter
237        list.filter();
238      } else {
239        // filter to this category
240        list.filter(function (item) {
241          const itemValues = item.values();
242          if (itemValues.categories !== null) {
243            const categories = decodeURIComponent(
244              atob(itemValues.categories)
245            ).split(",");
246            return categories.includes(category);
247          } else {
248            return false;
249          }
250        });
251      }
252    }
253  }
254}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.