From 8bfee04c8c6948a88114d53769d4bb89b8ec7bf5 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 17 Jun 2023 16:05:46 -0700
Subject: [PATCH] popovers

---
 package-lock.json                           | 14 +++++++
 package.json                                |  1 +
 quartz.config.ts                            |  1 +
 quartz/components/Content.tsx               | 30 +++++++++++---
 quartz/components/scripts/popover.inline.ts | 41 ++++++++++++++++++++
 quartz/components/scripts/toc.inline.ts     | 12 +++---
 quartz/components/styles/popover.scss       | 43 +++++++++++++++++++++
 quartz/plugins/emitters/contentPage.tsx     | 12 +++---
 quartz/plugins/index.ts                     |  3 +-
 quartz/plugins/transformers/links.ts        |  2 +
 10 files changed, 143 insertions(+), 16 deletions(-)
 create mode 100644 quartz/components/scripts/popover.inline.ts
 create mode 100644 quartz/components/styles/popover.scss

diff --git a/package-lock.json b/package-lock.json
index 6d922f4..eb3a121 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "4.0.3",
       "license": "MIT",
       "dependencies": {
+        "@floating-ui/dom": "^1.4.0",
         "@inquirer/prompts": "^1.0.3",
         "@napi-rs/simple-git": "^0.1.8",
         "chalk": "^4.1.2",
@@ -393,6 +394,19 @@
         "node": ">=12"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
+      "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.0.tgz",
+      "integrity": "sha512-b4F0iWffLiqb/TpP2PWVOixrZqE6ni+6VT64AmFH7sJIF3SFPLbe6/h3jQ5Cwffs+HaC9A8V0TQzCPBwVvziIA==",
+      "dependencies": {
+        "@floating-ui/core": "^1.3.1"
+      }
+    },
     "node_modules/@inquirer/checkbox": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-1.2.8.tgz",
diff --git a/package.json b/package.json
index 810cf5e..3e42cf7 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "quartz": "./quartz/bootstrap-cli.mjs"
   },
   "dependencies": {
+    "@floating-ui/dom": "^1.4.0",
     "@inquirer/prompts": "^1.0.3",
     "@napi-rs/simple-git": "^0.1.8",
     "chalk": "^4.1.2",
diff --git a/quartz.config.ts b/quartz.config.ts
index e18f8ba..5795672 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -64,6 +64,7 @@ const config: QuartzConfig = {
           Component.ReadingTime(),
           Component.TagList(),
         ],
+        content: Component.Content(),
         left: [
           Component.TableOfContents(),
         ],
diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx
index cc5d66a..0bcab1e 100644
--- a/quartz/components/Content.tsx
+++ b/quartz/components/Content.tsx
@@ -2,10 +2,30 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
 import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 
-function Content({ tree }: QuartzComponentProps) {
-  // @ts-ignore (preact makes it angry)
-  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
-  return <article>{content}</article>
+// @ts-ignore
+import popoverScript from './scripts/popover.inline'
+import popoverStyle from './styles/popover.scss'
+
+interface Options {
+  enablePopover: boolean
 }
 
-export default (() => Content) satisfies QuartzComponentConstructor
+const defaultOptions: Options = {
+  enablePopover: true
+}
+
+export default ((opts?: Partial<Options>) => {
+  function Content({ tree }: QuartzComponentProps) {
+    // @ts-ignore (preact makes it angry)
+    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
+    return <article>{content}</article>
+  }
+
+  const enablePopover = opts?.enablePopover ?? defaultOptions.enablePopover
+  if (enablePopover) {
+    Content.afterDOMLoaded = popoverScript
+    Content.css = popoverStyle
+  }
+
+  return Content
+}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts
new file mode 100644
index 0000000..24c6aec
--- /dev/null
+++ b/quartz/components/scripts/popover.inline.ts
@@ -0,0 +1,41 @@
+import { computePosition, inline, shift, autoPlacement } from "@floating-ui/dom"
+
+document.addEventListener("nav", () => {
+  const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
+  const p = new DOMParser()
+  for (const link of links) {
+    link.addEventListener("mouseenter", async ({ clientX, clientY }) => {
+      if (link.dataset.fetchedPopover === "true") return
+      const url = link.href
+      const contents = await fetch(`${url}`)
+        .then((res) => res.text())
+        .catch((err) => {
+          console.error(err)
+        })
+      if (!contents) return
+      const html = p.parseFromString(contents, "text/html")
+      const elts = [...html.getElementsByClassName("popover-hint")]
+      if (elts.length === 0) return
+
+
+      const popoverElement = document.createElement("div")
+      popoverElement.classList.add("popover")
+      elts.forEach(elt => popoverElement.appendChild(elt))
+
+      const { x, y } = await computePosition(link, popoverElement, {
+        middleware: [inline({
+          x: clientX,
+          y: clientY
+        }), shift(), autoPlacement()]
+      })
+
+      Object.assign(popoverElement.style, {
+        left: `${x}px`,
+        top: `${y}px`,
+      })
+
+      link.appendChild(popoverElement)
+      link.dataset.fetchedPopover = "true"
+    })
+  }
+})
diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts
index 105889d..d6cd50a 100644
--- a/quartz/components/scripts/toc.inline.ts
+++ b/quartz/components/scripts/toc.inline.ts
@@ -22,11 +22,13 @@ function toggleToc(this: HTMLElement) {
 }
 
 function setupToc() {
-  const toc = document.getElementById("toc")!
-  const content = toc.nextElementSibling as HTMLElement
-  content.style.maxHeight = content.scrollHeight + "px"
-  toc.removeEventListener("click", toggleToc)
-  toc.addEventListener("click", toggleToc)
+  const toc = document.getElementById("toc")
+  if (toc) {
+    const content = toc.nextElementSibling as HTMLElement
+    content.style.maxHeight = content.scrollHeight + "px"
+    toc.removeEventListener("click", toggleToc)
+    toc.addEventListener("click", toggleToc)
+  }
 }
 
 window.addEventListener("resize", setupToc)
diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss
new file mode 100644
index 0000000..0d26d7d
--- /dev/null
+++ b/quartz/components/styles/popover.scss
@@ -0,0 +1,43 @@
+@keyframes dropin {
+  0% {
+    opacity: 0;
+    visibility: hidden;
+  }
+  50% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+    visibility: visible;
+  }
+}
+
+.popover {
+  z-index: 999;
+  position: absolute;
+  overflow: scroll;
+  width: 30rem;
+  height: 20rem;
+  padding: 0 1rem;
+  margin-top: -1rem;
+  border: 1px solid var(--lightgray);
+  background-color: var(--light);
+  border-radius: 5px;
+  box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25);
+
+  font-weight: initial;
+
+  visibility: hidden;
+  opacity: 0;
+  transition: opacity 0.2s ease, visibility 0.2s ease;
+
+  @media all and (max-width: 600px) {
+    display: none !important;
+  }
+}
+
+a:hover .popover, .popover:hover {
+  animation: dropin 0.5s ease;
+  opacity: 1;
+  visibility: visible;
+}
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index ea626f5..4728920 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -6,12 +6,12 @@ import { resolveToRoot } from "../../path"
 import HeaderConstructor from "../../components/Header"
 import { QuartzComponentProps } from "../../components/types"
 import BodyConstructor from "../../components/Body"
-import ContentConstructor from "../../components/Content"
 
 interface Options {
   head: QuartzComponent
   header: QuartzComponent[],
   beforeBody: QuartzComponent[],
+  content: QuartzComponent,
   left: QuartzComponent[],
   right: QuartzComponent[],
   footer: QuartzComponent[],
@@ -25,12 +25,11 @@ export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
   const { head: Head, header, beforeBody, left, right, footer } = opts
   const Header = HeaderConstructor()
   const Body = BodyConstructor()
-  const Content = ContentConstructor()
 
   return {
     name: "ContentPage",
     getQuartzComponents() {
-      return [opts.head, Header, Body, ...opts.header, ...opts.beforeBody, ...opts.left, ...opts.right, ...opts.footer]
+      return [opts.head, Header, Body, ...opts.header, ...opts.beforeBody, opts.content, ...opts.left, ...opts.right, ...opts.footer]
     },
     async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
       const fps: string[] = []
@@ -54,6 +53,7 @@ export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
           tree
         }
 
+        const Content = opts.content
         const doc = <html>
           <Head {...componentData} />
           <body data-slug={file.data.slug}>
@@ -61,12 +61,14 @@ export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
               <Header {...componentData} >
                 {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
               </Header>
-              {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+              <div class="popover-hint">
+                {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+              </div>
               <Body {...componentData}>
                 <div class="left">
                   {left.map(BodyComponent => <BodyComponent {...componentData} />)}
                 </div>
-                <div class="center">
+                <div class="center popover-hint">
                   <Content {...componentData} />
                 </div>
                 <div class="right">
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 7e665fc..358c59e 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -14,7 +14,8 @@ export type ComponentResources = {
 }
 
 function joinScripts(scripts: string[]): string {
-  return scripts.join("\n")
+  // wrap with iife to prevent scope collision
+  return scripts.map(script => `(function () {${script}})();`).join("\n")
 }
 
 export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) {
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index 1619344..4bf0e08 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -48,6 +48,8 @@ export const ResolveLinks: QuartzTransformerPlugin<Partial<Options> | undefined>
               // don't process external links or intra-document anchors
               if (!(isAbsoluteUrl(node.properties.href) || node.properties.href.startsWith("#"))) {
                 node.properties.href = transformLink(node.properties.href)
+              } else {
+
               }
 
               // rewrite link internals if prettylinks is on