quartz/build.ts
@@ -19,6 +19,7 @@ import { Mutex } from "async-mutex" import DepGraph from "./depgraph" import { getStaticResourcesFromPlugins } from "./plugins" import { randomIdNonSecure } from "./util/random" type Dependencies = Record<string, DepGraph<FilePath> | null> @@ -38,13 +39,9 @@ type FileEvent = "add" | "change" | "delete" function newBuildId() { return Math.random().toString(36).substring(2, 8) } async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const ctx: BuildCtx = { buildId: newBuildId(), buildId: randomIdNonSecure(), argv, cfg, allSlugs: [], @@ -162,7 +159,7 @@ return } const buildId = newBuildId() const buildId = randomIdNonSecure() ctx.buildId = buildId buildData.lastBuildMs = new Date().getTime() const release = await mut.acquire() @@ -359,7 +356,7 @@ toRemove.add(filePath) } const buildId = newBuildId() const buildId = randomIdNonSecure() ctx.buildId = buildId buildData.lastBuildMs = new Date().getTime() const release = await mut.acquire() quartz/components/Backlinks.tsx
@@ -3,7 +3,7 @@ import { resolveRelative, simplifySlug } from "../util/path" import { i18n } from "../i18n" import { classNames } from "../util/lang" import OverflowList from "./OverflowList" import OverflowListFactory from "./OverflowList" interface BacklinksOptions { hideWhenEmpty: boolean @@ -15,6 +15,7 @@ export default ((opts?: Partial<BacklinksOptions>) => { const options: BacklinksOptions = { ...defaultOptions, ...opts } const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() const Backlinks: QuartzComponent = ({ fileData, @@ -30,7 +31,7 @@ return ( <div class={classNames(displayClass, "backlinks")}> <h3>{i18n(cfg.locale).components.backlinks.title}</h3> <OverflowList id="backlinks-ul"> <OverflowList> {backlinkFiles.length > 0 ? ( backlinkFiles.map((f) => ( <li> @@ -48,7 +49,7 @@ } Backlinks.css = style Backlinks.afterDOMLoaded = OverflowList.afterDOMLoaded("backlinks-ul") Backlinks.afterDOMLoaded = overflowListAfterDOMLoaded return Backlinks }) satisfies QuartzComponentConstructor quartz/components/Explorer.tsx
@@ -6,7 +6,8 @@ import { classNames } from "../util/lang" import { i18n } from "../i18n" import { FileTrieNode } from "../util/fileTrie" import OverflowList from "./OverflowList" import OverflowListFactory from "./OverflowList" import { concatenateResources } from "../util/resources" type OrderEntries = "sort" | "filter" | "map" @@ -56,6 +57,7 @@ export default ((userOpts?: Partial<Options>) => { const opts: Options = { ...defaultOptions, ...userOpts } const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => { return ( @@ -73,8 +75,7 @@ > <button type="button" id="mobile-explorer" class="explorer-toggle hide-until-loaded" class="explorer-toggle mobile-explorer hide-until-loaded" data-mobile={true} aria-controls="explorer-content" > @@ -95,8 +96,7 @@ </button> <button type="button" id="desktop-explorer" class="title-button explorer-toggle" class="title-button explorer-toggle desktop-explorer" data-mobile={false} aria-expanded={true} > @@ -116,8 +116,8 @@ <polyline points="6 9 12 15 18 9"></polyline> </svg> </button> <div id="explorer-content" aria-expanded={false}> <OverflowList id="explorer-ul" /> <div class="explorer-content" aria-expanded={false}> <OverflowList class="explorer-ul" /> </div> <template id="template-file"> <li> @@ -157,6 +157,6 @@ } Explorer.css = style Explorer.afterDOMLoaded = script + OverflowList.afterDOMLoaded("explorer-ul") Explorer.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded) return Explorer }) satisfies QuartzComponentConstructor quartz/components/OverflowList.tsx
@@ -1,22 +1,31 @@ import { JSX } from "preact" import { randomIdNonSecure } from "../util/random" const OverflowList = ({ children, ...props }: JSX.HTMLAttributes<HTMLUListElement> & { id: string }) => { return ( <ul class="overflow" {...props}> <ul {...props} class={[props.class, "overflow"].filter(Boolean).join(" ")} id={props.id}> {children} <li class="overflow-end" /> </ul> ) } OverflowList.afterDOMLoaded = (id: string) => ` export default () => { const id = randomIdNonSecure() return { OverflowList: (props: JSX.HTMLAttributes<HTMLUListElement>) => ( <OverflowList {...props} id={id} /> ), overflowListAfterDOMLoaded: ` document.addEventListener("nav", (e) => { const observer = new IntersectionObserver((entries) => { for (const entry of entries) { const parentUl = entry.target.parentElement if (!parentUl) return if (entry.isIntersecting) { parentUl.classList.remove("gradient-active") } else { @@ -34,6 +43,6 @@ observer.observe(end) window.addCleanup(() => observer.disconnect()) }) ` export default OverflowList `, } } quartz/components/TableOfContents.tsx
@@ -6,7 +6,8 @@ // @ts-ignore import script from "./scripts/toc.inline" import { i18n } from "../i18n" import OverflowList from "./OverflowList" import OverflowListFactory from "./OverflowList" import { concatenateResources } from "../util/resources" interface Options { layout: "modern" | "legacy" @@ -16,6 +17,9 @@ layout: "modern", } export default ((opts?: Partial<Options>) => { const layout = opts?.layout ?? defaultOptions.layout const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() const TableOfContents: QuartzComponent = ({ fileData, displayClass, @@ -50,7 +54,7 @@ </svg> </button> <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}> <OverflowList id="toc-ul"> <OverflowList> {fileData.toc.map((tocEntry) => ( <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> @@ -63,8 +67,9 @@ </div> ) } TableOfContents.css = modernStyle TableOfContents.afterDOMLoaded = script + OverflowList.afterDOMLoaded("toc-ul") TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded) const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { if (!fileData.toc) { @@ -89,7 +94,5 @@ } LegacyTableOfContents.css = legacyStyle export default ((opts?: Partial<Options>) => { const layout = opts?.layout ?? defaultOptions.layout return layout === "modern" ? TableOfContents : LegacyTableOfContents }) satisfies QuartzComponentConstructor quartz/components/pages/FolderContent.tsx
@@ -9,6 +9,7 @@ import { i18n } from "../../i18n" import { QuartzPluginData } from "../../plugins/vfile" import { ComponentChildren } from "preact" import { concatenateResources } from "../../util/resources" interface FolderContentOptions { /** @@ -104,6 +105,6 @@ ) } FolderContent.css = style + PageList.css FolderContent.css = concatenateResources(style, PageList.css) return FolderContent }) satisfies QuartzComponentConstructor quartz/components/pages/TagContent.tsx
@@ -7,6 +7,7 @@ import { htmlToJsx } from "../../util/jsx" import { i18n } from "../../i18n" import { ComponentChildren } from "preact" import { concatenateResources } from "../../util/resources" interface TagContentOptions { sort?: SortFn @@ -124,6 +125,6 @@ } } TagContent.css = style + PageList.css TagContent.css = concatenateResources(style, PageList.css) return TagContent }) satisfies QuartzComponentConstructor quartz/components/scripts/explorer.inline.ts
@@ -21,15 +21,14 @@ let currentExplorerState: Array<FolderState> function toggleExplorer(this: HTMLElement) { const explorers = document.querySelectorAll(".explorer") for (const explorer of explorers) { explorer.classList.toggle("collapsed") explorer.setAttribute( const nearestExplorer = this.closest(".explorer") as HTMLElement if (!nearestExplorer) return nearestExplorer.classList.toggle("collapsed") nearestExplorer.setAttribute( "aria-expanded", explorer.getAttribute("aria-expanded") === "true" ? "false" : "true", nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true", ) } } function toggleFolder(evt: MouseEvent) { evt.stopPropagation() @@ -145,7 +144,7 @@ } async function setupExplorer(currentSlug: FullSlug) { const allExplorers = document.querySelectorAll(".explorer") as NodeListOf<HTMLElement> const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf<HTMLElement> for (const explorer of allExplorers) { const dataFns = JSON.parse(explorer.dataset.dataFns || "{}") @@ -192,7 +191,7 @@ collapsed: oldIndex.get(path) === true, })) const explorerUl = document.getElementById("explorer-ul") const explorerUl = explorer.querySelector(".explorer-ul") if (!explorerUl) continue // Create and insert new content @@ -219,14 +218,12 @@ } // Set up event handlers const explorerButtons = explorer.querySelectorAll( "button.explorer-toggle", ) as NodeListOf<HTMLElement> if (explorerButtons) { window.addCleanup(() => explorerButtons.forEach((button) => button.removeEventListener("click", toggleExplorer)), ) explorerButtons.forEach((button) => button.addEventListener("click", toggleExplorer)) const explorerButtons = explorer.getElementsByClassName( "explorer-toggle", ) as HTMLCollectionOf<HTMLElement> for (const button of explorerButtons) { button.addEventListener("click", toggleExplorer) window.addCleanup(() => button.removeEventListener("click", toggleExplorer)) } // Set up folder click handlers @@ -235,8 +232,8 @@ "folder-button", ) as HTMLCollectionOf<HTMLElement> for (const button of folderButtons) { window.addCleanup(() => button.removeEventListener("click", toggleFolder)) button.addEventListener("click", toggleFolder) window.addCleanup(() => button.removeEventListener("click", toggleFolder)) } } @@ -244,15 +241,15 @@ "folder-icon", ) as HTMLCollectionOf<HTMLElement> for (const icon of folderIcons) { window.addCleanup(() => icon.removeEventListener("click", toggleFolder)) icon.addEventListener("click", toggleFolder) window.addCleanup(() => icon.removeEventListener("click", toggleFolder)) } } } document.addEventListener("prenav", async (e: CustomEventMap["prenav"]) => { document.addEventListener("prenav", async () => { // save explorer scrollTop position const explorer = document.getElementById("explorer-ul") const explorer = document.querySelector(".explorer-ul") if (!explorer) return sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString()) }) @@ -262,9 +259,8 @@ await setupExplorer(currentSlug) // if mobile hamburger is visible, collapse by default const mobileExplorer = document.getElementById("mobile-explorer") if (mobileExplorer && mobileExplorer.checkVisibility()) { for (const explorer of document.querySelectorAll(".explorer")) { for (const explorer of document.getElementsByClassName("mobile-explorer")) { if (explorer.checkVisibility()) { explorer.classList.add("collapsed") explorer.setAttribute("aria-expanded", "false") } quartz/components/styles/explorer.scss
@@ -20,7 +20,7 @@ margin: 0; } .hide-until-loaded ~ #explorer-content { .hide-until-loaded ~ .explorer-content { display: none; } } @@ -30,6 +30,8 @@ display: flex; flex-direction: column; overflow-y: hidden; min-height: 1.2rem; flex: 0 1 auto; &.collapsed { flex: 0 1 1.2rem; @@ -52,20 +54,20 @@ align-self: flex-start; } button#mobile-explorer { button.mobile-explorer { display: none; } button#desktop-explorer { button.desktop-explorer { display: flex; } @media all and ($mobile) { button#mobile-explorer { button.mobile-explorer { display: flex; } button#desktop-explorer { button.desktop-explorer { display: none; } } @@ -86,8 +88,8 @@ } } button#mobile-explorer, button#desktop-explorer { button.mobile-explorer, button.desktop-explorer { background-color: transparent; border: none; text-align: left; @@ -104,7 +106,7 @@ } } #explorer-content { .explorer-content { list-style: none; overflow: hidden; overflow-y: auto; @@ -209,7 +211,7 @@ &.collapsed { flex: 0 0 34px; & > #explorer-content { & > .explorer-content { transform: translateX(-100vw); visibility: hidden; } @@ -218,13 +220,13 @@ &:not(.collapsed) { flex: 0 0 34px; & > #explorer-content { & > .explorer-content { transform: translateX(0); visibility: visible; } } #explorer-content { .explorer-content { box-sizing: border-box; z-index: 100; position: absolute; @@ -245,7 +247,7 @@ visibility: hidden; } #mobile-explorer { .mobile-explorer { margin: 0; padding: 5px; z-index: 101; quartz/components/styles/toc.scss
@@ -5,6 +5,7 @@ flex-direction: column; overflow-y: hidden; min-height: 4rem; flex: 0 1 auto; &:has(button.toc-header.collapsed) { flex: 0 1 1.2rem; quartz/components/types.ts
@@ -1,5 +1,5 @@ import { ComponentType, JSX } from "preact" import { StaticResources } from "../util/resources" import { StaticResources, StringResource } from "../util/resources" import { QuartzPluginData } from "../plugins/vfile" import { GlobalConfiguration } from "../cfg" import { Node } from "hast" @@ -19,9 +19,9 @@ } export type QuartzComponent = ComponentType<QuartzComponentProps> & { css?: string beforeDOMLoaded?: string afterDOMLoaded?: string css?: StringResource beforeDOMLoaded?: StringResource afterDOMLoaded?: StringResource } export type QuartzComponentConstructor<Options extends object | undefined = undefined> = ( quartz/plugins/emitters/componentResources.ts
@@ -36,17 +36,21 @@ afterDOMLoaded: new Set<string>(), } function normalizeResource(resource: string | string[] | undefined): string[] { if (!resource) return [] if (Array.isArray(resource)) return resource return [resource] } for (const component of allComponents) { const { css, beforeDOMLoaded, afterDOMLoaded } = component if (css) { componentResources.css.add(css) } if (beforeDOMLoaded) { componentResources.beforeDOMLoaded.add(beforeDOMLoaded) } if (afterDOMLoaded) { componentResources.afterDOMLoaded.add(afterDOMLoaded) } const normalizedCss = normalizeResource(css) const normalizedBeforeDOMLoaded = normalizeResource(beforeDOMLoaded) const normalizedAfterDOMLoaded = normalizeResource(afterDOMLoaded) normalizedCss.forEach((c) => componentResources.css.add(c)) normalizedBeforeDOMLoaded.forEach((b) => componentResources.beforeDOMLoaded.add(b)) normalizedAfterDOMLoaded.forEach((a) => componentResources.afterDOMLoaded.add(a)) } return { quartz/styles/base.scss
@@ -542,7 +542,7 @@ } .spacer { flex: 1 1 auto; flex: 2 1 auto; } div:has(> .overflow) { @@ -555,17 +555,14 @@ max-height: 100%; overflow-y: auto; width: 100%; margin-bottom: 0; // clearfix content: ""; clear: both; & > li:last-of-type { margin-bottom: 30px; } & > li.overflow-end { height: 4px; height: 1rem; margin: 0; } quartz/util/random.ts
New file @@ -0,0 +1,3 @@ export function randomIdNonSecure() { return Math.random().toString(36).substring(2, 8) } quartz/util/resources.tsx
@@ -65,3 +65,10 @@ js: JSResource[] additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[] } export type StringResource = string | string[] | undefined export function concatenateResources(...resources: StringResource[]): StringResource { return resources .filter((resource): resource is string | string[] => resource !== undefined) .flat() }