From 659cbc474378ab956d77e5c79b49c56d89c49fd2 Mon Sep 17 00:00:00 2001 From: Jacky Zhao 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 6d922f47..eb3a121c 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 810cf5e9..3e42cf70 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 e18f8ba2..57956720 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 cc5d66aa..0bcab1e2 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
{content}
+// @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) => { + function Content({ tree }: QuartzComponentProps) { + // @ts-ignore (preact makes it angry) + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) + return
{content}
+ } + + 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 00000000..24c6aecb --- /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 105889d7..d6cd50af 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 00000000..0d26d7d3 --- /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 ea626f5c..4728920e 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 = (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 { const fps: string[] = [] @@ -54,6 +53,7 @@ export const ContentPage: QuartzEmitterPlugin = (opts) => { tree } + const Content = opts.content const doc = @@ -61,12 +61,14 @@ export const ContentPage: QuartzEmitterPlugin = (opts) => {
{header.map(HeaderComponent => )}
- {beforeBody.map(BodyComponent => )} +
+ {beforeBody.map(BodyComponent => )} +
{left.map(BodyComponent => )}
-
+
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index 7e665fc7..358c59e0 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 16193442..4bf0e08e 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -48,6 +48,8 @@ export const ResolveLinks: QuartzTransformerPlugin | 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