Jacky Zhao
2023-06-17 8bfee04c8c6948a88114d53769d4bb89b8ec7bf5
popovers
2 files added
8 files modified
141 ■■■■■ changed files
package-lock.json 14 ●●●●● patch | view | raw | blame | history
package.json 1 ●●●● patch | view | raw | blame | history
quartz.config.ts 1 ●●●● patch | view | raw | blame | history
quartz/components/Content.tsx 22 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/popover.inline.ts 41 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/toc.inline.ts 4 ●●● patch | view | raw | blame | history
quartz/components/styles/popover.scss 43 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentPage.tsx 10 ●●●●● patch | view | raw | blame | history
quartz/plugins/index.ts 3 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 2 ●●●●● patch | view | raw | blame | history
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",
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",
quartz.config.ts
@@ -64,6 +64,7 @@
          Component.ReadingTime(),
          Component.TagList(),
        ],
        content: Component.Content(),
        left: [
          Component.TableOfContents(),
        ],
quartz/components/Content.tsx
@@ -2,10 +2,30 @@
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
// @ts-ignore
import popoverScript from './scripts/popover.inline'
import popoverStyle from './styles/popover.scss'
interface Options {
  enablePopover: boolean
}
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>
}
export default (() => Content) satisfies QuartzComponentConstructor
  const enablePopover = opts?.enablePopover ?? defaultOptions.enablePopover
  if (enablePopover) {
    Content.afterDOMLoaded = popoverScript
    Content.css = popoverStyle
  }
  return Content
}) satisfies QuartzComponentConstructor
quartz/components/scripts/popover.inline.ts
New file
@@ -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"
    })
  }
})
quartz/components/scripts/toc.inline.ts
@@ -22,12 +22,14 @@
}
function setupToc() {
  const toc = document.getElementById("toc")!
  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)
document.addEventListener("nav", () => {
quartz/components/styles/popover.scss
New file
@@ -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;
}
quartz/plugins/emitters/contentPage.tsx
@@ -6,12 +6,12 @@
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 @@
  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 @@
          tree
        }
        const Content = opts.content
        const doc = <html>
          <Head {...componentData} />
          <body data-slug={file.data.slug}>
@@ -61,12 +61,14 @@
              <Header {...componentData} >
                {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
              </Header>
              <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">
quartz/plugins/index.ts
@@ -14,7 +14,8 @@
}
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) {
quartz/plugins/transformers/links.ts
@@ -48,6 +48,8 @@
              // 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