{% bibliography %}
diff --git a/_sass/_base.scss b/_sass/_base.scss
index 90bdeb0..1cc3372 100644
--- a/_sass/_base.scss
+++ b/_sass/_base.scss
@@ -1149,7 +1149,8 @@ ninja-keys::part(ninja-input-wrapper) {
width: 100%;
}
-.newsletter-form-input {
+.newsletter-form-input,
+.bibsearch-form-input {
color: var(--global-newsletter-text-color);
background: var(--global-newsletter-bg-color);
border: 1px solid var(--global-newsletter-text-color);
@@ -1287,3 +1288,9 @@ ninja-keys::part(ninja-input-wrapper) {
border-bottom-color: var(--global-divider-color);
}
}
+
+// highlight-search-term
+::highlight(search) {
+ background-color: var(--global-theme-color);
+ color: var(--global-text-color);
+}
diff --git a/assets/js/bibsearch.js b/assets/js/bibsearch.js
new file mode 100644
index 0000000..8bc50d8
--- /dev/null
+++ b/assets/js/bibsearch.js
@@ -0,0 +1,67 @@
+import { highlightSearchTerm } from "./highlight-search-term.js";
+
+document.addEventListener("DOMContentLoaded", function () {
+ // actual bibsearch logic
+ const filterItems = (searchTerm) => {
+ document.querySelectorAll(".bibliography, .unloaded").forEach((element) => element.classList.remove("unloaded"));
+
+ // highlight-search-term
+ if (CSS.highlights) {
+ const nonMatchingElements = highlightSearchTerm({ search: searchTerm, selector: ".bibliography > li" });
+ nonMatchingElements.forEach((element) => {
+ element.classList.add("unloaded");
+ });
+ } else {
+ // Simply add unloaded class to all non-matching items if Browser does not support CSS highlights
+ document.querySelectorAll(".bibliography > li").forEach((element, index) => {
+ const text = element.innerText.toLowerCase();
+ if (text.indexOf(searchTerm) == -1) {
+ element.classList.add("unloaded");
+ }
+ });
+ }
+
+ document.querySelectorAll("h2.bibliography").forEach(function (element) {
+ let iterator = element.nextElementSibling; // get next sibling element after h2, which can be h3 or ol
+ let hideFirstGroupingElement = true;
+ // iterate until next group element (h2), which is already selected by the querySelectorAll(-).forEach(-)
+ while (iterator && iterator.tagName !== "H2") {
+ if (iterator.tagName === "OL") {
+ const ol = iterator;
+ const unloadedSiblings = ol.querySelectorAll(":scope > li.unloaded");
+ const totalSiblings = ol.querySelectorAll(":scope > li");
+
+ if (unloadedSiblings.length === totalSiblings.length) {
+ ol.previousElementSibling.classList.add("unloaded"); // Add the '.unloaded' class to the previous grouping element (e.g. year)
+ ol.classList.add("unloaded"); // Add the '.unloaded' class to the OL itself
+ } else {
+ hideFirstGroupingElement = false; // there is at least some visible entry, don't hide the first grouping element
+ }
+ }
+ iterator = iterator.nextElementSibling;
+ }
+ // Add unloaded class to first grouping element (e.g. year) if no item left in this group
+ if (hideFirstGroupingElement) {
+ element.classList.add("unloaded");
+ }
+ });
+ };
+
+ const updateInputField = () => {
+ const hashValue = decodeURIComponent(window.location.hash.substring(1)); // Remove the '#' character
+ document.getElementById("bibsearch").value = hashValue;
+ filterItems(hashValue);
+ };
+
+ // Sensitive search. Only start searching if there's been no input for 300 ms
+ let timeoutId;
+ document.getElementById("bibsearch").addEventListener("input", function () {
+ clearTimeout(timeoutId); // Clear the previous timeout
+ const searchTerm = this.value.toLowerCase();
+ timeoutId = setTimeout(filterItems(searchTerm), 300);
+ });
+
+ window.addEventListener("hashchange", updateInputField); // Update the filter when the hash changes
+
+ updateInputField(); // Update filter when page loads
+});
diff --git a/assets/js/highlight-search-term.js b/assets/js/highlight-search-term.js
new file mode 100644
index 0000000..d74a22c
--- /dev/null
+++ b/assets/js/highlight-search-term.js
@@ -0,0 +1,110 @@
+/**
+ * This file is a modified version of:
+ * https://github.com/marmelab/highlight-search-term/blob/main/src/index.js
+ * - We return the `nonMatchingElements`
+ * - We fixed a bug: `getRangesForSearchTermInElement` got the `node.parentElement`, which is not working if there are multiple text nodes in one element.
+ *
+ * highlight-search-term is published under MIT License.
+ *
+ * MIT License
+ *
+ * Copyright (c) 2024 marmelab
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * Highlight search term in the selected elements
+ *
+ * @example
+ * import { highlightSearchTerm } from "highlight-search-term";
+ * const search = document.getElementById("search");
+ * search.addEventListener("input", () => {
+ * highlightSearchTerm({ search: search.value, selector: ".content" });
+ * });
+ */
+const highlightSearchTerm = ({ search, selector, customHighlightName = "search" }) => {
+ if (!selector) {
+ throw new Error("The selector argument is required");
+ }
+
+ if (!CSS.highlights) return; // disable feature on Firefox as it does not support CSS Custom Highlight API
+
+ // remove previous highlight
+ CSS.highlights.delete(customHighlightName);
+ if (!search) {
+ // nothing to highlight
+ return;
+ }
+ // find all text nodes containing the search term
+ const ranges = [];
+ const nonMatchingElements = [];
+ const elements = document.querySelectorAll(selector);
+ Array.from(elements).map((element) => {
+ let match = false;
+ getTextNodesInElementContainingText(element, search).forEach((node) => {
+ // Modified variant of highlight-search-term
+ // We return the non-matching elements in addition.
+ const rangesForSearch = getRangesForSearchTermInNode(node, search);
+ ranges.push(...rangesForSearch);
+ if (rangesForSearch.length > 0) {
+ match = true;
+ }
+ });
+ if (!match) {
+ nonMatchingElements.push(element);
+ }
+ });
+ if (ranges.length === 0) return nonMatchingElements; // modified: return `nonMatchingElements`
+ // create a CSS highlight that can be styled with the ::highlight(search) pseudo-element
+ const highlight = new Highlight(...ranges);
+ CSS.highlights.set(customHighlightName, highlight);
+ return nonMatchingElements; // modified: return `nonMatchingElements`
+};
+
+const getTextNodesInElementContainingText = (element, text) => {
+ const nodes = [];
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
+ let node;
+ while ((node = walker.nextNode())) {
+ if (node.textContent && node.textContent.toLowerCase().includes(text)) {
+ nodes.push(node);
+ }
+ }
+ return nodes;
+};
+
+// Fix: We changed this function to work on the node directly, rather than on its parent element.
+const getRangesForSearchTermInNode = (node, search) => {
+ const ranges = [];
+ const text = (node.textContent ? node.textContent.toLowerCase() : "") || "";
+
+ let start = 0;
+ let index;
+ while ((index = text.indexOf(search, start)) >= 0) {
+ const range = new Range();
+ range.setStart(node, index);
+ range.setEnd(node, index + search.length);
+ ranges.push(range);
+ start = index + search.length;
+ }
+ return ranges;
+};
+
+export { highlightSearchTerm };