/* We need to refresh the scroll spy after (un)hiding elements */ const refreshScrollSpy = () =>{ const dataSpyList = [].slice.call(document.querySelectorAll('[data-bs-spy="scroll"]')); dataSpyList.forEach((dataSpyEl) => { bootstrap.ScrollSpy.getInstance(dataSpyEl) .refresh() }) } const propagateScrollSpyURL = () => { window.addEventListener('activate.bs.scrollspy', (e) => { history.replaceState({}, "", e.relatedTarget); }); } /* Show or hide elements based on their tag */ const toggleTagVisibility = (tagName) => { const tag = tags.find((element) => { return element.name === tagName; }); tag.visible = !tag.visible; const hiddenTagNames = tags.map((tag) => { if (tag.visible) { return } else { return tag.name } }); const elements = $(`[data-mne-tags~="${tagName}"]`); elements.each((i) => { const currentElement = elements[i]; const tagValuesOfCurrentElement = currentElement.getAttribute('data-mne-tags'); // TODO This can probably be refactored to not use a Set. const tagNamesOfCurrentElement = new Set(tagValuesOfCurrentElement.match(/\S+/g)); // non-whitespace const visibleTagNamesOfCurrentElement = new Set( [...tagNamesOfCurrentElement].filter(e => !hiddenTagNames.includes(e)) ); if (visibleTagNamesOfCurrentElement.size === 0) { // hide $(currentElement).slideToggle('fast', () => { currentElement.classList.add('d-none'); }); } else if ($(currentElement).hasClass('d-none')) { // show currentElement.classList.remove('d-none'); $(currentElement).slideToggle('fast'); } }) const tagBadgeElements = document.querySelectorAll(`span.badge[data-mne-tag~="${tagName}"]`); tagBadgeElements.forEach((badgeElement) => { if (tag.visible) { badgeElement.removeAttribute('data-mne-tag-hidden'); badgeElement.classList.remove('bg-secondary'); badgeElement.classList.add('bg-primary'); } else { badgeElement.setAttribute('data-mne-tag-hidden', true); badgeElement.classList.remove('bg-primary'); badgeElement.classList.add('bg-secondary'); } }) refreshScrollSpy(); } /* Gather all available tags and expose them in the global namespace */ let tags = []; // array of objects const gatherTags = () => { // only consider top-level elements const taggedElements = document.querySelectorAll("#content > div[data-mne-tags]"); taggedElements.forEach((element) => { const value = element.getAttribute('data-mne-tags'); const tagNames = value.match(/\S+/g); // non-whitespace tagNames.forEach((tagName) => { const existingTag = tags.find((element) => { return element.name === tagName; }) if (existingTag === undefined) { const tag = { name : tagName, visible: true, count: 1 }; tags.push(tag); } else { existingTag.count = existingTag.count + 1; } }) }) } /* Badges do display the tag count */ const updateTagCountBadges = () => { const menuEntries = document .querySelectorAll("#filter-by-tags-dropdown-menu > ul > li > label[data-mne-tag]") menuEntries.forEach((menuEntry) => { const tagName = menuEntry.getAttribute('data-mne-tag'); const tag = tags.find((tag) => { return tag.name === tagName; }) const tagCount = tag.count; const tagCountBadge = menuEntry.querySelector('span.badge'); tagCountBadge.innerHTML = tagCount.toString(); }); } const addFilterByTagsCheckboxEventHandlers = () => { // "Filter by tag" checkbox event handling const selectAllTagsCheckboxLabel = document .querySelector('#selectAllTagsCheckboxLabel'); const filterByTagsDropdownMenuLabels = document .querySelectorAll("#filter-by-tags-dropdown-menu > ul > li > label[data-mne-tag]") filterByTagsDropdownMenuLabels.forEach((label) => { // Prevent dropdown menu from closing when clicking on a tag checkbox label label.addEventListener("click", (e) => { e.stopPropagation(); }) // Show / hide content if a tag checkbox value has changed const tagName = label.getAttribute("data-mne-tag"); const checkbox = label.querySelector("input"); checkbox.addEventListener("change", () => { toggleTagVisibility(tagName); }) }) // "Select all" checkbox selectAllTagsCheckboxLabel.addEventListener("click", (e) => { e.stopPropagation(); }) const selectAllTagsCheckbox = selectAllTagsCheckboxLabel.querySelector('input'); selectAllTagsCheckbox.addEventListener("change", (e) => { const selectAllCheckboxStatus = e.target.checked; filterByTagsDropdownMenuLabels.forEach((element) => { const checkbox = element.querySelector('input'); if (checkbox.checked !== selectAllCheckboxStatus) { checkbox.checked = selectAllCheckboxStatus // we need to manually trigger the change event const changeEvent = new Event('change'); checkbox.dispatchEvent(changeEvent); } }) }); } /* Avoid top of content getting hidden behind navbar after clicking on a TOC link */ const _handleTocLinkClick = (e) => { e.preventDefault(); const topBarHeight = document.querySelector('#top-bar').scrollHeight const margin = 30 + topBarHeight; const tocLinkElement = e.target; const targetDomId = tocLinkElement.getAttribute('href'); const targetElement = document.querySelector(targetDomId); const top = targetElement.getBoundingClientRect().top + window.scrollY; // Update URL to reflect the current scroll position. // We use history.pushState to change the URL without causing the browser to scroll. history.pushState(null, "", targetDomId); // Now scroll to the correct position. window.scrollTo(0, top - margin); } const fixScrollingForTocLinks = () => { const tocLinkElements = document.querySelectorAll('#toc-navbar > a'); tocLinkElements.forEach((element) => { element.removeEventListener('click', _handleTocLinkClick) element.addEventListener('click', _handleTocLinkClick) }) } const addSliderEventHandlers = () => { const accordionElementsWithSlider = document.querySelectorAll('div.accordion-item.slider'); accordionElementsWithSlider.forEach((el) => { const accordionElement = el.querySelector('div.accordion-body'); const slider = accordionElement.querySelector('input'); // const sliderLabel = accordionElement.querySelector('label'); const carousel = accordionElement.querySelector('div.carousel'); slider.addEventListener('input', (e) => { const sliderValue = parseInt(e.target.value); $(carousel).carousel(sliderValue); }) // Allow focussing the slider with a click on the slider or carousel, so keyboard // controls (left / right arrow) can be enabled. // This also appears to be the only way to focus the slider in Safari: // https://itnext.io/fixing-focus-for-safari-b5916fef1064?gi=c1b8b043fa9b slider.addEventListener('click', () => { slider.focus({preventScroll: true}) }) carousel.addEventListener('click', () => { slider.focus({preventScroll: true}) }) }) } /* Avoid top of content gets hidden behind the top navbar */ const fixTopMargin = () => { const topBarHeight = document.querySelector('#top-bar').scrollHeight const margin = 30 + topBarHeight; document.getElementById('content').style.marginTop = `${margin}px`; document.getElementById('toc').style.marginTop = `${margin}px`; } /* Show / hide all tags on keypress */ const _globalKeyHandler = (e) => { if (e.code === "KeyT") { const selectAllTagsCheckbox = document .querySelector('#selectAllTagsCheckboxLabel > input'); selectAllTagsCheckbox.checked = !selectAllTagsCheckbox.checked; // we need to manually trigger the change event const changeEvent = new Event('change'); selectAllTagsCheckbox.dispatchEvent(changeEvent); } } const enableGlobalKeyHandler = () => { window.onkeydown = (e) => _globalKeyHandler(e); } const disableGlobalKeyHandler = () => { window.onkeydown = null; } /* Disable processing global key events when a search box is active */ const disableGlobalKeysInSearchBox = () => { const searchBoxElements = document.querySelectorAll('input.search-input'); searchBoxElements.forEach((el) => { el.addEventListener('focus', () => disableGlobalKeyHandler()); el.addEventListener('blur', () => enableGlobalKeyHandler()); }) } /* Run once all content is fully loaded. */ window.addEventListener('load', () => { gatherTags(); updateTagCountBadges(); addFilterByTagsCheckboxEventHandlers(); addSliderEventHandlers(); fixTopMargin(); fixScrollingForTocLinks(); hljs.highlightAll(); // enable highlight.js disableGlobalKeysInSearchBox(); enableGlobalKeyHandler(); propagateScrollSpyURL(); }); /* Resizing the window throws off the scroll spy and top-margin handling. */ window.onresize = () => { fixTopMargin(); refreshScrollSpy(); };