fix(graph): make graph non-singleton, proper cleanup, fix radial
This commit is contained in:
		@@ -48,7 +48,7 @@ const defaultOptions: GraphOptions = {
 | 
			
		||||
    depth: -1,
 | 
			
		||||
    scale: 0.9,
 | 
			
		||||
    repelForce: 0.5,
 | 
			
		||||
    centerForce: 0.3,
 | 
			
		||||
    centerForce: 0.2,
 | 
			
		||||
    linkDistance: 30,
 | 
			
		||||
    fontSize: 0.6,
 | 
			
		||||
    opacityScale: 1,
 | 
			
		||||
@@ -67,8 +67,8 @@ export default ((opts?: Partial<GraphOptions>) => {
 | 
			
		||||
      <div class={classNames(displayClass, "graph")}>
 | 
			
		||||
        <h3>{i18n(cfg.locale).components.graph.title}</h3>
 | 
			
		||||
        <div class="graph-outer">
 | 
			
		||||
          <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
 | 
			
		||||
          <button id="global-graph-icon" aria-label="Global Graph">
 | 
			
		||||
          <div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
 | 
			
		||||
          <button class="global-graph-icon" aria-label="Global Graph">
 | 
			
		||||
            <svg
 | 
			
		||||
              version="1.1"
 | 
			
		||||
              xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
@@ -95,8 +95,8 @@ export default ((opts?: Partial<GraphOptions>) => {
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="global-graph-outer">
 | 
			
		||||
          <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
 | 
			
		||||
        <div class="global-graph-outer">
 | 
			
		||||
          <div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -68,11 +68,9 @@ type TweenNode = {
 | 
			
		||||
  stop: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
 | 
			
		||||
  const slug = simplifySlug(fullSlug)
 | 
			
		||||
  const visited = getVisited()
 | 
			
		||||
  const graph = document.getElementById(container)
 | 
			
		||||
  if (!graph) return
 | 
			
		||||
  removeAllChildren(graph)
 | 
			
		||||
 | 
			
		||||
  let {
 | 
			
		||||
@@ -167,16 +165,14 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
  const height = Math.max(graph.offsetHeight, 250)
 | 
			
		||||
 | 
			
		||||
  // we virtualize the simulation and use pixi to actually render it
 | 
			
		||||
  // Calculate the radius of the container circle
 | 
			
		||||
  const radius = Math.min(width, height) / 2 - 40 // 40px padding
 | 
			
		||||
  const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
 | 
			
		||||
    .force("charge", forceManyBody().strength(-100 * repelForce))
 | 
			
		||||
    .force("center", forceCenter().strength(centerForce))
 | 
			
		||||
    .force("link", forceLink(graphData.links).distance(linkDistance))
 | 
			
		||||
    .force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
 | 
			
		||||
 | 
			
		||||
  if (enableRadial)
 | 
			
		||||
    simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
 | 
			
		||||
  const radius = (Math.min(width, height) / 2) * 0.8
 | 
			
		||||
  if (enableRadial) simulation.force("radial", forceRadial(radius).strength(0.2))
 | 
			
		||||
 | 
			
		||||
  // precompute style prop strings as pixi doesn't support css variables
 | 
			
		||||
  const cssVars = [
 | 
			
		||||
@@ -524,7 +520,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let stopAnimation = false
 | 
			
		||||
  function animate(time: number) {
 | 
			
		||||
    if (stopAnimation) return
 | 
			
		||||
    for (const n of nodeRenderData) {
 | 
			
		||||
      const { x, y } = n.simulationData
 | 
			
		||||
      if (!x || !y) continue
 | 
			
		||||
@@ -548,61 +546,101 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
    requestAnimationFrame(animate)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const graphAnimationFrameHandle = requestAnimationFrame(animate)
 | 
			
		||||
  window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
 | 
			
		||||
  requestAnimationFrame(animate)
 | 
			
		||||
  return () => {
 | 
			
		||||
    stopAnimation = true
 | 
			
		||||
    app.destroy()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let localGraphCleanups: (() => void)[] = []
 | 
			
		||||
let globalGraphCleanups: (() => void)[] = []
 | 
			
		||||
 | 
			
		||||
function cleanupLocalGraphs() {
 | 
			
		||||
  for (const cleanup of localGraphCleanups) {
 | 
			
		||||
    cleanup()
 | 
			
		||||
  }
 | 
			
		||||
  localGraphCleanups = []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cleanupGlobalGraphs() {
 | 
			
		||||
  for (const cleanup of globalGraphCleanups) {
 | 
			
		||||
    cleanup()
 | 
			
		||||
  }
 | 
			
		||||
  globalGraphCleanups = []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
 | 
			
		||||
  const slug = e.detail.url
 | 
			
		||||
  addToVisited(simplifySlug(slug))
 | 
			
		||||
  await renderGraph("graph-container", slug)
 | 
			
		||||
 | 
			
		||||
  // Function to re-render the graph when the theme changes
 | 
			
		||||
  const handleThemeChange = () => {
 | 
			
		||||
    renderGraph("graph-container", slug)
 | 
			
		||||
  async function renderLocalGraph() {
 | 
			
		||||
    cleanupLocalGraphs()
 | 
			
		||||
    const localGraphContainers = document.getElementsByClassName("graph-container")
 | 
			
		||||
    for (const container of localGraphContainers) {
 | 
			
		||||
      localGraphCleanups.push(await renderGraph(container as HTMLElement, slug))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // event listener for theme change
 | 
			
		||||
  document.addEventListener("themechange", handleThemeChange)
 | 
			
		||||
  await renderLocalGraph()
 | 
			
		||||
  const handleThemeChange = () => {
 | 
			
		||||
    void renderLocalGraph()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // cleanup for the event listener
 | 
			
		||||
  document.addEventListener("themechange", handleThemeChange)
 | 
			
		||||
  window.addCleanup(() => {
 | 
			
		||||
    document.removeEventListener("themechange", handleThemeChange)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const container = document.getElementById("global-graph-outer")
 | 
			
		||||
  const sidebar = container?.closest(".sidebar") as HTMLElement
 | 
			
		||||
 | 
			
		||||
  function renderGlobalGraph() {
 | 
			
		||||
  const containers = [...document.getElementsByClassName("global-graph-outer")] as HTMLElement[]
 | 
			
		||||
  async function renderGlobalGraph() {
 | 
			
		||||
    const slug = getFullSlug(window)
 | 
			
		||||
    container?.classList.add("active")
 | 
			
		||||
    if (sidebar) {
 | 
			
		||||
      sidebar.style.zIndex = "1"
 | 
			
		||||
    }
 | 
			
		||||
    for (const container of containers) {
 | 
			
		||||
      container.classList.add("active")
 | 
			
		||||
      const sidebar = container.closest(".sidebar") as HTMLElement
 | 
			
		||||
      if (sidebar) {
 | 
			
		||||
        sidebar.style.zIndex = "1"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    renderGraph("global-graph-container", slug)
 | 
			
		||||
    registerEscapeHandler(container, hideGlobalGraph)
 | 
			
		||||
      const graphContainer = container.querySelector(".global-graph-container") as HTMLElement
 | 
			
		||||
      registerEscapeHandler(container, hideGlobalGraph)
 | 
			
		||||
      if (graphContainer) {
 | 
			
		||||
        globalGraphCleanups.push(await renderGraph(graphContainer, slug))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function hideGlobalGraph() {
 | 
			
		||||
    container?.classList.remove("active")
 | 
			
		||||
    if (sidebar) {
 | 
			
		||||
      sidebar.style.zIndex = ""
 | 
			
		||||
    cleanupGlobalGraphs()
 | 
			
		||||
    for (const container of containers) {
 | 
			
		||||
      container.classList.remove("active")
 | 
			
		||||
      const sidebar = container.closest(".sidebar") as HTMLElement
 | 
			
		||||
      if (sidebar) {
 | 
			
		||||
        sidebar.style.zIndex = ""
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
 | 
			
		||||
    if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
      const globalGraphOpen = container?.classList.contains("active")
 | 
			
		||||
      globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
 | 
			
		||||
      const anyGlobalGraphOpen = containers.some((container) =>
 | 
			
		||||
        container.classList.contains("active"),
 | 
			
		||||
      )
 | 
			
		||||
      anyGlobalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const containerIcon = document.getElementById("global-graph-icon")
 | 
			
		||||
  containerIcon?.addEventListener("click", renderGlobalGraph)
 | 
			
		||||
  window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
 | 
			
		||||
  const containerIcons = document.getElementsByClassName("global-graph-icon")
 | 
			
		||||
  Array.from(containerIcons).forEach((icon) => {
 | 
			
		||||
    icon.addEventListener("click", renderGlobalGraph)
 | 
			
		||||
    window.addCleanup(() => icon.removeEventListener("click", renderGlobalGraph))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  document.addEventListener("keydown", shortcutHandler)
 | 
			
		||||
  window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
 | 
			
		||||
  window.addCleanup(() => {
 | 
			
		||||
    document.removeEventListener("keydown", shortcutHandler)
 | 
			
		||||
    cleanupLocalGraphs()
 | 
			
		||||
    cleanupGlobalGraphs()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -384,7 +384,7 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
 | 
			
		||||
    preview.replaceChildren(previewInner)
 | 
			
		||||
 | 
			
		||||
    // scroll to longest
 | 
			
		||||
    const highlights = [...preview.querySelectorAll(".highlight")].sort(
 | 
			
		||||
    const highlights = [...preview.getElementsByClassName("highlight")].sort(
 | 
			
		||||
      (a, b) => b.innerHTML.length - a.innerHTML.length,
 | 
			
		||||
    )
 | 
			
		||||
    highlights[0]?.scrollIntoView({ block: "start" })
 | 
			
		||||
@@ -488,7 +488,7 @@ async function fillDocument(data: ContentIndex) {
 | 
			
		||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
 | 
			
		||||
  const currentSlug = e.detail.url
 | 
			
		||||
  const data = await fetchData
 | 
			
		||||
  const searchElement = document.querySelectorAll(".search")
 | 
			
		||||
  const searchElement = document.getElementsByClassName("search")
 | 
			
		||||
  for (const element of searchElement) {
 | 
			
		||||
    await setupSearch(element, currentSlug, data)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ function toggleToc(this: HTMLElement) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupToc() {
 | 
			
		||||
  for (const toc of document.querySelectorAll(".toc")) {
 | 
			
		||||
  for (const toc of document.getElementsByClassName("toc")) {
 | 
			
		||||
    const button = toc.querySelector(".toc-header")
 | 
			
		||||
    const content = toc.querySelector(".toc-content")
 | 
			
		||||
    if (!button || !content) return
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    & > #global-graph-icon {
 | 
			
		||||
    & > .global-graph-icon {
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      background: none;
 | 
			
		||||
      border: none;
 | 
			
		||||
@@ -38,7 +38,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > #global-graph-outer {
 | 
			
		||||
  & > .global-graph-outer {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    z-index: 9999;
 | 
			
		||||
    left: 0;
 | 
			
		||||
@@ -53,7 +53,7 @@
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > #global-graph-container {
 | 
			
		||||
    & > .global-graph-container {
 | 
			
		||||
      border: 1px solid var(--lightgray);
 | 
			
		||||
      background-color: var(--light);
 | 
			
		||||
      border-radius: 5px;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user