Path: blob/main/src/resources/formats/html/quarto.js
12922 views
import * as tabsets from "./tabsets/tabsets.js";12const sectionChanged = new CustomEvent("quarto-sectionChanged", {3detail: {},4bubbles: true,5cancelable: false,6composed: false,7});89const layoutMarginEls = () => {10// Find any conflicting margin elements and add margins to the11// top to prevent overlap12const marginChildren = window.document.querySelectorAll(13".column-margin.column-container > *, .margin-caption, .aside"14);1516let lastBottom = 0;17for (const marginChild of marginChildren) {18if (marginChild.offsetParent !== null) {19// clear the top margin so we recompute it20marginChild.style.marginTop = null;21const top = marginChild.getBoundingClientRect().top + window.scrollY;22if (top < lastBottom) {23const marginChildStyle = window.getComputedStyle(marginChild);24const marginBottom = parseFloat(marginChildStyle["marginBottom"]);25const margin = lastBottom - top + marginBottom;26marginChild.style.marginTop = `${margin}px`;27}28const styles = window.getComputedStyle(marginChild);29const marginTop = parseFloat(styles["marginTop"]);30lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;31}32}33};3435window.document.addEventListener("DOMContentLoaded", function (_event) {36// Recompute the position of margin elements anytime the body size changes37if (window.ResizeObserver) {38const resizeObserver = new window.ResizeObserver(39throttle(() => {40layoutMarginEls();41if (42window.document.body.getBoundingClientRect().width < 990 &&43isReaderMode()44) {45quartoToggleReader();46}47}, 50)48);49resizeObserver.observe(window.document.body);50}5152const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');53const sidebarEl = window.document.getElementById("quarto-sidebar");54const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");55const marginSidebarEl = window.document.getElementById(56"quarto-margin-sidebar"57);58// function to determine whether the element has a previous sibling that is active59const prevSiblingIsActiveLink = (el) => {60const sibling = el.previousElementSibling;61if (sibling && sibling.tagName === "A") {62return sibling.classList.contains("active");63} else {64return false;65}66};6768// dispatch for htmlwidgets69// they use slideenter event to trigger resize70function fireSlideEnter() {71const event = window.document.createEvent("Event");72event.initEvent("slideenter", true, true);73window.document.dispatchEvent(event);74}7576const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');77tabs.forEach((tab) => {78tab.addEventListener("shown.bs.tab", fireSlideEnter);79});8081// dispatch for shiny82// they use BS shown and hidden events to trigger rendering83function distpatchShinyEvents(previous, current) {84if (window.jQuery) {85if (previous) {86window.jQuery(previous).trigger("hidden");87}88if (current) {89window.jQuery(current).trigger("shown");90}91}92}9394// tabby.js listener: Trigger event for htmlwidget and shiny95document.addEventListener(96"tabby",97function (event) {98fireSlideEnter();99distpatchShinyEvents(event.detail.previousTab, event.detail.tab);100},101false102);103104// Track scrolling and mark TOC links as active105// get table of contents and sidebar (bail if we don't have at least one)106const tocLinks = tocEl107? [...tocEl.querySelectorAll("a[data-scroll-target]")]108: [];109const makeActive = (link) => tocLinks[link].classList.add("active");110const removeActive = (link) => tocLinks[link].classList.remove("active");111const removeAllActive = () =>112[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));113114// activate the anchor for a section associated with this TOC entry115tocLinks.forEach((link) => {116link.addEventListener("click", () => {117if (link.href.indexOf("#") !== -1) {118const anchor = link.href.split("#")[1];119const heading = window.document.querySelector(120`[data-anchor-id="${anchor}"]`121);122if (heading) {123// Add the class124heading.classList.add("reveal-anchorjs-link");125126// function to show the anchor127const handleMouseout = () => {128heading.classList.remove("reveal-anchorjs-link");129heading.removeEventListener("mouseout", handleMouseout);130};131132// add a function to clear the anchor when the user mouses out of it133heading.addEventListener("mouseout", handleMouseout);134}135}136});137});138139const sections = tocLinks.map((link) => {140const target = link.getAttribute("data-scroll-target");141if (target.startsWith("#")) {142return window.document.getElementById(decodeURI(`${target.slice(1)}`));143} else {144return window.document.querySelector(decodeURI(`${target}`));145}146});147148const sectionMargin = 200;149let currentActive = 0;150// track whether we've initialized state the first time151let init = false;152153const updateActiveLink = () => {154// The index from bottom to top (e.g. reversed list)155let sectionIndex = -1;156if (157window.innerHeight + window.pageYOffset >=158window.document.body.offsetHeight159) {160// This is the no-scroll case where last section should be the active one161sectionIndex = 0;162} else {163// This finds the last section visible on screen that should be made active164sectionIndex = [...sections].reverse().findIndex((section) => {165if (section) {166return window.pageYOffset >= section.offsetTop - sectionMargin;167} else {168return false;169}170});171}172if (sectionIndex > -1) {173const current = sections.length - sectionIndex - 1;174if (current !== currentActive) {175removeAllActive();176currentActive = current;177makeActive(current);178if (init) {179window.dispatchEvent(sectionChanged);180}181init = true;182}183}184};185186const inHiddenRegion = (top, bottom, hiddenRegions) => {187for (const region of hiddenRegions) {188if (top <= region.bottom && bottom >= region.top) {189return true;190}191}192return false;193};194195const categorySelector = "header.quarto-title-block .quarto-category";196const activateCategories = (href) => {197// Find any categories198// Surround them with a link pointing back to:199// #category=Authoring200try {201const categoryEls = window.document.querySelectorAll(categorySelector);202for (const categoryEl of categoryEls) {203const categoryText = categoryEl.textContent;204if (categoryText) {205const link = `${href}#category=${encodeURIComponent(categoryText)}`;206const linkEl = window.document.createElement("a");207linkEl.setAttribute("href", link);208for (const child of categoryEl.childNodes) {209linkEl.append(child);210}211categoryEl.appendChild(linkEl);212}213}214} catch {215// Ignore errors216}217};218function hasTitleCategories() {219return window.document.querySelector(categorySelector) !== null;220}221222function offsetRelativeUrl(url) {223const offset = getMeta("quarto:offset");224return offset ? offset + url : url;225}226227function offsetAbsoluteUrl(url) {228const offset = getMeta("quarto:offset");229const baseUrl = new URL(offset, window.location);230231const projRelativeUrl = url.replace(baseUrl, "");232if (projRelativeUrl.startsWith("/")) {233return projRelativeUrl;234} else {235return "/" + projRelativeUrl;236}237}238239// read a meta tag value240function getMeta(metaName) {241const metas = window.document.getElementsByTagName("meta");242for (let i = 0; i < metas.length; i++) {243if (metas[i].getAttribute("name") === metaName) {244return metas[i].getAttribute("content");245}246}247return "";248}249250async function findAndActivateCategories() {251// Categories search with listing only use path without query252const currentPagePath = offsetAbsoluteUrl(253window.location.origin + window.location.pathname254);255const response = await fetch(offsetRelativeUrl("listings.json"));256if (response.status == 200) {257return response.json().then(function (listingPaths) {258const listingHrefs = [];259for (const listingPath of listingPaths) {260const pathWithoutLeadingSlash = listingPath.listing.substring(1);261for (const item of listingPath.items) {262const encodedItem = encodeURI(item);263if (264encodedItem === currentPagePath ||265encodedItem === currentPagePath + "index.html"266) {267// Resolve this path against the offset to be sure268// we already are using the correct path to the listing269// (this adjusts the listing urls to be rooted against270// whatever root the page is actually running against)271const relative = offsetRelativeUrl(pathWithoutLeadingSlash);272const baseUrl = window.location;273const resolvedPath = new URL(relative, baseUrl);274listingHrefs.push(resolvedPath.pathname);275break;276}277}278}279280// Look up the tree for a nearby linting and use that if we find one281const nearestListing = findNearestParentListing(282offsetAbsoluteUrl(window.location.pathname),283listingHrefs284);285if (nearestListing) {286activateCategories(nearestListing);287} else {288// See if the referrer is a listing page for this item289const referredRelativePath = offsetAbsoluteUrl(document.referrer);290const referrerListing = listingHrefs.find((listingHref) => {291const isListingReferrer =292listingHref === referredRelativePath ||293listingHref === referredRelativePath + "index.html";294return isListingReferrer;295});296297if (referrerListing) {298// Try to use the referrer if possible299activateCategories(referrerListing);300} else if (listingHrefs.length > 0) {301// Otherwise, just fall back to the first listing302activateCategories(listingHrefs[0]);303}304}305});306}307}308if (hasTitleCategories()) {309findAndActivateCategories();310}311312const findNearestParentListing = (href, listingHrefs) => {313if (!href || !listingHrefs) {314return undefined;315}316// Look up the tree for a nearby linting and use that if we find one317const relativeParts = href.substring(1).split("/");318while (relativeParts.length > 0) {319const path = relativeParts.join("/");320for (const listingHref of listingHrefs) {321if (listingHref.startsWith(path)) {322return listingHref;323}324}325relativeParts.pop();326}327328return undefined;329};330331const manageSidebarVisiblity = (el, placeholderDescriptor) => {332let isVisible = true;333let elRect;334335return (hiddenRegions) => {336if (el === null) {337return;338}339340// Find the last element of the TOC341const lastChildEl = el.lastElementChild;342343if (lastChildEl) {344// Converts the sidebar to a menu345const convertToMenu = () => {346for (const child of el.children) {347child.style.opacity = 0;348child.style.overflow = "hidden";349child.style.pointerEvents = "none";350}351352nexttick(() => {353const toggleContainer = window.document.createElement("div");354toggleContainer.style.width = "100%";355toggleContainer.classList.add("zindex-over-content");356toggleContainer.classList.add("quarto-sidebar-toggle");357toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom358toggleContainer.id = placeholderDescriptor.id;359toggleContainer.style.position = "fixed";360361const toggleIcon = window.document.createElement("i");362toggleIcon.classList.add("quarto-sidebar-toggle-icon");363toggleIcon.classList.add("bi");364toggleIcon.classList.add("bi-caret-down-fill");365366const toggleTitle = window.document.createElement("div");367const titleEl = window.document.body.querySelector(368placeholderDescriptor.titleSelector369);370if (titleEl) {371toggleTitle.append(372titleEl.textContent || titleEl.innerText,373toggleIcon374);375}376toggleTitle.classList.add("zindex-over-content");377toggleTitle.classList.add("quarto-sidebar-toggle-title");378toggleContainer.append(toggleTitle);379380const toggleContents = window.document.createElement("div");381toggleContents.classList = el.classList;382toggleContents.classList.add("zindex-over-content");383toggleContents.classList.add("quarto-sidebar-toggle-contents");384for (const child of el.children) {385if (child.id === "toc-title") {386continue;387}388389const clone = child.cloneNode(true);390clone.style.opacity = 1;391clone.style.pointerEvents = null;392clone.style.display = null;393toggleContents.append(clone);394}395toggleContents.style.height = "0px";396const positionToggle = () => {397// position the element (top left of parent, same width as parent)398if (!elRect) {399elRect = el.getBoundingClientRect();400}401toggleContainer.style.left = `${elRect.left}px`;402toggleContainer.style.top = `${elRect.top}px`;403toggleContainer.style.width = `${elRect.width}px`;404};405positionToggle();406407toggleContainer.append(toggleContents);408el.parentElement.prepend(toggleContainer);409410// Process clicks411let tocShowing = false;412// Allow the caller to control whether this is dismissed413// when it is clicked (e.g. sidebar navigation supports414// opening and closing the nav tree, so don't dismiss on click)415const clickEl = placeholderDescriptor.dismissOnClick416? toggleContainer417: toggleTitle;418419const closeToggle = () => {420if (tocShowing) {421toggleContainer.classList.remove("expanded");422toggleContents.style.height = "0px";423tocShowing = false;424}425};426427// Get rid of any expanded toggle if the user scrolls428window.document.addEventListener(429"scroll",430throttle(() => {431closeToggle();432}, 50)433);434435// Handle positioning of the toggle436window.addEventListener(437"resize",438throttle(() => {439elRect = undefined;440positionToggle();441}, 50)442);443444window.addEventListener("quarto-hrChanged", () => {445elRect = undefined;446});447448// Process the click449clickEl.onclick = () => {450if (!tocShowing) {451toggleContainer.classList.add("expanded");452toggleContents.style.height = null;453tocShowing = true;454} else {455closeToggle();456}457};458});459};460461// Converts a sidebar from a menu back to a sidebar462const convertToSidebar = () => {463for (const child of el.children) {464child.style.opacity = 1;465child.style.overflow = null;466child.style.pointerEvents = null;467}468469const placeholderEl = window.document.getElementById(470placeholderDescriptor.id471);472if (placeholderEl) {473placeholderEl.remove();474}475476el.classList.remove("rollup");477};478479if (isReaderMode()) {480convertToMenu();481isVisible = false;482} else {483// Find the top and bottom o the element that is being managed484const elTop = el.offsetTop;485const elBottom =486elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;487488if (!isVisible) {489// If the element is current not visible reveal if there are490// no conflicts with overlay regions491if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {492convertToSidebar();493isVisible = true;494}495} else {496// If the element is visible, hide it if it conflicts with overlay regions497// and insert a placeholder toggle (or if we're in reader mode)498if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {499convertToMenu();500isVisible = false;501}502}503}504}505};506};507508const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');509for (const tabEl of tabEls) {510const id = tabEl.getAttribute("data-bs-target");511if (id) {512const columnEl = document.querySelector(513`${id} .column-margin, .tabset-margin-content`514);515if (columnEl)516tabEl.addEventListener("shown.bs.tab", function (event) {517const el = event.srcElement;518if (el) {519const visibleCls = `${el.id}-margin-content`;520// walk up until we find a parent tabset521let panelTabsetEl = el.parentElement;522while (panelTabsetEl) {523if (panelTabsetEl.classList.contains("panel-tabset")) {524break;525}526panelTabsetEl = panelTabsetEl.parentElement;527}528529if (panelTabsetEl) {530const prevSib = panelTabsetEl.previousElementSibling;531if (532prevSib &&533prevSib.classList.contains("tabset-margin-container")534) {535const childNodes = prevSib.querySelectorAll(536".tabset-margin-content"537);538for (const childEl of childNodes) {539if (childEl.classList.contains(visibleCls)) {540childEl.classList.remove("collapse");541} else {542childEl.classList.add("collapse");543}544}545}546}547}548549layoutMarginEls();550});551}552}553554// Manage the visibility of the toc and the sidebar555const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {556id: "quarto-toc-toggle",557titleSelector: "#toc-title",558dismissOnClick: true,559});560const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {561id: "quarto-sidebarnav-toggle",562titleSelector: ".title",563dismissOnClick: false,564});565let tocLeftScrollVisibility;566if (leftTocEl) {567tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {568id: "quarto-lefttoc-toggle",569titleSelector: "#toc-title",570dismissOnClick: true,571});572}573574// Find the first element that uses formatting in special columns575const conflictingEls = window.document.body.querySelectorAll(576'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'577);578579// Filter all the possibly conflicting elements into ones580// the do conflict on the left or ride side581const arrConflictingEls = Array.from(conflictingEls);582const leftSideConflictEls = arrConflictingEls.filter((el) => {583if (el.tagName === "ASIDE") {584return false;585}586return Array.from(el.classList).find((className) => {587return (588className !== "column-body" &&589className.startsWith("column-") &&590!className.endsWith("right") &&591!className.endsWith("container") &&592className !== "column-margin"593);594});595});596const rightSideConflictEls = arrConflictingEls.filter((el) => {597if (el.tagName === "ASIDE") {598return true;599}600601const hasMarginCaption = Array.from(el.classList).find((className) => {602return className == "margin-caption";603});604if (hasMarginCaption) {605return true;606}607608return Array.from(el.classList).find((className) => {609return (610className !== "column-body" &&611!className.endsWith("container") &&612className.startsWith("column-") &&613!className.endsWith("left")614);615});616});617618const kOverlapPaddingSize = 10;619function toRegions(els) {620return els.map((el) => {621const boundRect = el.getBoundingClientRect();622const top =623boundRect.top +624document.documentElement.scrollTop -625kOverlapPaddingSize;626return {627top,628bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,629};630});631}632633let hasObserved = false;634const visibleItemObserver = (els) => {635let visibleElements = [...els];636const intersectionObserver = new IntersectionObserver(637(entries, _observer) => {638entries.forEach((entry) => {639if (entry.isIntersecting) {640if (visibleElements.indexOf(entry.target) === -1) {641visibleElements.push(entry.target);642}643} else {644visibleElements = visibleElements.filter((visibleEntry) => {645return visibleEntry !== entry;646});647}648});649650if (!hasObserved) {651hideOverlappedSidebars();652}653hasObserved = true;654},655{}656);657els.forEach((el) => {658intersectionObserver.observe(el);659});660661return {662getVisibleEntries: () => {663return visibleElements;664},665};666};667668const rightElementObserver = visibleItemObserver(rightSideConflictEls);669const leftElementObserver = visibleItemObserver(leftSideConflictEls);670671const hideOverlappedSidebars = () => {672marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));673sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));674if (tocLeftScrollVisibility) {675tocLeftScrollVisibility(676toRegions(leftElementObserver.getVisibleEntries())677);678}679};680681window.quartoToggleReader = () => {682// Applies a slow class (or removes it)683// to update the transition speed684const slowTransition = (slow) => {685const manageTransition = (id, slow) => {686const el = document.getElementById(id);687if (el) {688if (slow) {689el.classList.add("slow");690} else {691el.classList.remove("slow");692}693}694};695696manageTransition("TOC", slow);697manageTransition("quarto-sidebar", slow);698};699const readerMode = !isReaderMode();700setReaderModeValue(readerMode);701702// If we're entering reader mode, slow the transition703if (readerMode) {704slowTransition(readerMode);705}706highlightReaderToggle(readerMode);707hideOverlappedSidebars();708709// If we're exiting reader mode, restore the non-slow transition710if (!readerMode) {711slowTransition(!readerMode);712}713};714715const highlightReaderToggle = (readerMode) => {716const els = document.querySelectorAll(".quarto-reader-toggle");717if (els) {718els.forEach((el) => {719if (readerMode) {720el.classList.add("reader");721} else {722el.classList.remove("reader");723}724});725}726};727728const setReaderModeValue = (val) => {729if (window.location.protocol !== "file:") {730window.localStorage.setItem("quarto-reader-mode", val);731} else {732localReaderMode = val;733}734};735736const isReaderMode = () => {737if (window.location.protocol !== "file:") {738return window.localStorage.getItem("quarto-reader-mode") === "true";739} else {740return localReaderMode;741}742};743let localReaderMode = null;744745const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");746const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;747748// Walk the TOC and collapse/expand nodes749// Nodes are expanded if:750// - they are top level751// - they have children that are 'active' links752// - they are directly below an link that is 'active'753const walk = (el, depth) => {754// Tick depth when we enter a UL755if (el.tagName === "UL") {756depth = depth + 1;757}758759// It this is active link760let isActiveNode = false;761if (el.tagName === "A" && el.classList.contains("active")) {762isActiveNode = true;763}764765// See if there is an active child to this element766let hasActiveChild = false;767for (const child of el.children) {768hasActiveChild = walk(child, depth) || hasActiveChild;769}770771// Process the collapse state if this is an UL772if (el.tagName === "UL") {773if (tocOpenDepth === -1 && depth > 1) {774// toc-expand: false775el.classList.add("collapse");776} else if (777depth <= tocOpenDepth ||778hasActiveChild ||779prevSiblingIsActiveLink(el)780) {781el.classList.remove("collapse");782} else {783el.classList.add("collapse");784}785786// untick depth when we leave a UL787depth = depth - 1;788}789return hasActiveChild || isActiveNode;790};791792// walk the TOC and expand / collapse any items that should be shown793if (tocEl) {794updateActiveLink();795walk(tocEl, 0);796}797798// Throttle the scroll event and walk peridiocally799window.document.addEventListener(800"scroll",801throttle(() => {802if (tocEl) {803updateActiveLink();804walk(tocEl, 0);805}806if (!isReaderMode()) {807hideOverlappedSidebars();808}809}, 5)810);811window.addEventListener(812"resize",813throttle(() => {814if (tocEl) {815updateActiveLink();816walk(tocEl, 0);817}818if (!isReaderMode()) {819hideOverlappedSidebars();820}821}, 10)822);823hideOverlappedSidebars();824highlightReaderToggle(isReaderMode());825});826827tabsets.init();828829function throttle(func, wait) {830let waiting = false;831return function () {832if (!waiting) {833func.apply(this, arguments);834waiting = true;835setTimeout(function () {836waiting = false;837}, wait);838}839};840}841842function nexttick(func) {843return setTimeout(func, 0);844}845846847