initial commit

This commit is contained in:
2025-08-19 09:13:22 -07:00
parent 28464811d6
commit 0977a3e14d
820 changed files with 1003358 additions and 2 deletions

View File

@@ -0,0 +1,269 @@
/* 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();
};