popovers
This commit is contained in:
		
							parent
							
								
									cb89cce183
								
							
						
					
					
						commit
						8bfee04c8c
					
				
					 10 changed files with 143 additions and 16 deletions
				
			
		
							
								
								
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,6 +64,7 @@ const config: QuartzConfig = {
 | 
			
		|||
          Component.ReadingTime(),
 | 
			
		||||
          Component.TagList(),
 | 
			
		||||
        ],
 | 
			
		||||
        content: Component.Content(),
 | 
			
		||||
        left: [
 | 
			
		||||
          Component.TableOfContents(),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										41
									
								
								quartz/components/scripts/popover.inline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								quartz/components/scripts/popover.inline.ts
									
										
									
									
									
										Normal 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"
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -22,11 +22,13 @@ function toggleToc(this: HTMLElement) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										43
									
								
								quartz/components/styles/popover.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								quartz/components/styles/popover.scss
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
              <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">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue