添加移动段导航目录

This commit is contained in:
WANGYQ\wangyq 2024-10-30 10:52:40 +08:00
parent a3cec9cc97
commit 5e94c3e813
12 changed files with 544 additions and 1 deletions

View File

@ -8,7 +8,7 @@ import * as Plugin from "./quartz/plugins"
*/
const config: QuartzConfig = {
configuration: {
pageTitle: "🪴 cris的obsidian分享",
pageTitle: "🪴 cris' blog",
pageTitleSuffix: "",
enableSPA: true,
enablePopovers: true,

View File

@ -1,5 +1,6 @@
import { PageLayout, SharedLayout } from "./quartz/cfg"
import * as Component from "./quartz/components"
import * as ExtraComponent from "./quartz/extra/components"
// components shared across all pages
export const sharedPageComponents: SharedLayout = {
@ -52,6 +53,7 @@ export const defaultContentPageLayout: PageLayout = {
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.MobileOnly(ExtraComponent.OverlayExplorer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(
@ -93,6 +95,7 @@ export const defaultListPageLayout: PageLayout = {
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.MobileOnly(ExtraComponent.OverlayExplorer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(Component.Explorer()),

View File

@ -0,0 +1,218 @@
// Nothing yet
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../components/types"
import overlayexplorerStyle from "./styles/overlayexplorer.scss"
// @ts-ignore
import script from "./scripts/overlayexplorer.inline"
import { FileNode, Options } from "../components/ExplorerNode"
import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
import { joinSegments, resolveRelative } from "../util/path"
interface OlOptions extends Omit<Options, "folderClickBehavior"> {
folderClickBehavior: "collapse" | "link" | "mixed"
}
const defaultOptions = {
folderClickBehavior: "mixed",
folderDefaultState: "collapsed",
useSavedState: true,
mapFn: (node) => {
return node
},
sortFn: (a, b) => {
// Sort order: folders first, then files. Sort folders and files alphabetically
if ((!a.file && !b.file) || (a.file && b.file)) {
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
return a.displayName.localeCompare(b.displayName, undefined, {
numeric: true,
sensitivity: "base",
})
}
if (a.file && !b.file) {
return 1
} else {
return -1
}
},
filterFn: (node) => node.name !== "tags",
order: ["filter", "map", "sort"],
} satisfies OlOptions
type OlExplorerNodeProps = {
node: FileNode
opts: OlOptions
fileData: QuartzPluginData
fullPath?: string
}
function OverlayExplorerNode({node, opts, fullPath, fileData}: OlExplorerNodeProps) {
// Calculate current folderPath
const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
return (
<>
{node.file ? (
<li key={node.file.slug}>
<a href={resolveRelative(fileData.slug!, node.file.slug!)}>
{node.displayName}
</a>
</li>
) : (
<li>
{node.name !== "" && (
<div data-ol-selector-for={folderPath} class="ol-folder-entry">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="5 8 14 8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={`ol-folder-icon ${opts.folderDefaultState === "open" && "open"}`}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
{opts.folderClickBehavior === "link" ? (
<a href={href} class="ol-folder-title">
{node.displayName}
</a>
) : (
<>
<button class="ol-folder-button">
<span class="ol-folder-title">{node.displayName}</span>
</button>
{opts.folderClickBehavior === "mixed" && (
<a href={href}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="12"
viewBox="0 4 21 15"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="0 9 18 9"></polyline>
<polyline points="0 15 18 15"></polyline>
<polyline points="15 6 21 12 15 18"></polyline>
</svg>
</a>
)}
</>
)}
</div>
)}
<div data-ol-children-for={folderPath} class={`ol-folder-outer ${(node.depth === 0 || opts.folderDefaultState === "open") && "open"}`}>
<ul
style={{
paddingLeft: node.name !== "" ? "1.4rem" : "0",
}}
>
{node.children.map((childNode, i) => (
<OverlayExplorerNode
node={childNode}
key={i}
opts={opts}
fullPath={folderPath}
fileData={fileData}
/>
))}
</ul>
</div>
</li>
)}
</>
)
}
export default ((userOpts?: Partial<OlOptions>) => {
// Parse config
const opts: OlOptions = { ...defaultOptions, ...userOpts }
// memoized
let fileTree: FileNode
let lastBuildId: string = ""
function constructFileTree(allFiles: QuartzPluginData[]) {
// Construct tree from allFiles
fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file))
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
if (opts.order) {
// Order is important, use loop with index instead of order.map()
for (let i = 0; i < opts.order.length; i++) {
const functionName = opts.order[i]
if (functionName === "map") {
fileTree.map(opts.mapFn)
} else if (functionName === "sort") {
fileTree.sort(opts.sortFn)
} else if (functionName === "filter") {
fileTree.filter(opts.filterFn)
}
}
}
}
const OverlayExplorer: QuartzComponent = ({
ctx,
cfg,
allFiles,
displayClass,
fileData,
}: QuartzComponentProps) => {
if (ctx.buildId !== lastBuildId) {
lastBuildId = ctx.buildId
constructFileTree(allFiles)
}
return (
<div class={classNames(displayClass, "overlay-explorer")}>
<button
type="button"
id="overlay-explorer-button"
aria-controls="overlay-explorer-content"
data-olsavestate={opts.useSavedState}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="5 8 14 8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 6 18 6"></polyline>
<polyline points="6 12 18 12"></polyline>
<polyline points="6 18 18 18"></polyline>
</svg>
</button>
<div id="overlay-explorer-container">
<div id="overlay-explorer-space">
<div id="overlay-explorer-content">
<ul id="overlay-explorer-ul">
<OverlayExplorerNode node={fileTree} opts={opts} fileData={fileData} />
</ul>
</div>
</div>
</div>
</div>
)
}
OverlayExplorer.css = overlayexplorerStyle
OverlayExplorer.afterDOMLoaded = script
return OverlayExplorer
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,23 @@
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
import pageiconStyle from "./styles/pageicon.scss"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../components/types"
export default (() => {
const PageIcon: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => {
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
const iconPath = joinSegments(baseDir, "static/page_icon.png")
return (
<a href={baseDir}>
<img src={iconPath} alt={title} class="pageicon"/>
</a>
)
}
PageIcon.css = pageiconStyle
return PageIcon
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,7 @@
import OverlayExplorer from "./OverlayExplorer"
import PageIcon from "./PageIcon"
export {
OverlayExplorer,
PageIcon,
}

View File

@ -0,0 +1,108 @@
// Nothing yet
import { registerEscapeHandler } from "../../components/scripts/util"
type MaybeHTMLElement = HTMLElement | undefined
function setFolder(folderPath: string, open: boolean) {
const childrenList = document.querySelector(
`[data-ol-children-for='${folderPath}']`
) as MaybeHTMLElement
if (!childrenList) return
const folderEntry = document.querySelector(
`[data-ol-selector-for='${folderPath}']`
) as MaybeHTMLElement
if (!folderEntry) return
const collapseIcon = folderEntry.getElementsByTagName(
"svg"
)[0] as MaybeHTMLElement
if (!collapseIcon) return
if (open) {
childrenList.classList.add("open")
collapseIcon.classList.add("open")
} else {
childrenList.classList.remove("open")
collapseIcon.classList.remove("open")
}
}
function setupOverlayExplorer() {
const openButton = document.getElementById("overlay-explorer-button")
const container = document.getElementById("overlay-explorer-container")
const useSaveState = openButton?.dataset.olsavestate === "true"
let folderOpenMap: Map<string, boolean>
if (useSaveState) {
const fromStorage = localStorage.getItem("olFileTree")
folderOpenMap = new Map<string, boolean>(fromStorage ? JSON.parse(fromStorage) : [])
for (let [key, value] of folderOpenMap) {
setFolder(key, value)
}
}
function showExplorer() {
container?.classList.add("active")
}
function hideExplorer() {
container?.classList.remove("active")
}
function toggleFolder(evt: MouseEvent) {
evt.stopPropagation()
const target = evt.target as MaybeHTMLElement
if (!target) return
const folderPath = target.parentNode.getAttribute("data-ol-selector-for")
const childrenList = document.querySelector(
`[data-ol-children-for='${folderPath}']`
) as MaybeHTMLElement
if (!childrenList) return
const collapseIcon = target.parentNode.getElementsByTagName(
"svg"
)[0] as MaybeHTMLElement
if (!collapseIcon) return
childrenList.classList.toggle("open")
collapseIcon.classList.toggle("open")
if (useSaveState) {
folderOpenMap.set(folderPath, collapseIcon.classList.contains("open"))
localStorage.setItem(
"olFileTree",
JSON.stringify(Array.from(folderOpenMap.entries()))
)
}
}
openButton.addEventListener("click", showExplorer)
window.addCleanup(() => openButton.removeEventListener("click", showExplorer))
// Set up click handlers for each folder (click handler on folder "icon")
for (const item of document.getElementsByClassName(
"ol-folder-icon",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
for (const item of document.getElementsByClassName(
"ol-folder-button",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
registerEscapeHandler(container, hideExplorer)
}
document.addEventListener("nav", () => {
setupOverlayExplorer()
})

View File

@ -0,0 +1,164 @@
// Nothing yet
@use "../../styles/variables.scss" as *;
.ol-folder-icon {
margin-right: 5px;
color: var(--secondary);
cursor: pointer;
transition: transform 0.3s ease;
backface-visibility: visible;
transform: rotate(-90deg);
}
.ol-folder-icon.open {
transform: rotate(0deg);
}
.ol-folder-outer {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
}
.ol-folder-outer.open {
grid-template-rows: 1fr;
}
.ol-folder-outer > ul {
overflow: hidden;
}
.ol-folder-entry {
flex-direction: row;
display: flex;
align-items: center;
user-select: none;
}
.ol-folder-button {
color: var(--dark);
background-color: transparent;
border: none;
text-align: left;
cursor: pointer;
padding-left: 0;
padding-right: 0;
display: flex;
align-items: center;
font-family: var(--headerFont);
margin-right: 5px;
& span {
font-size: 0.95rem;
display: inline-block;
color: var(--secondary);
font-weight: $semiBoldWeight;
margin: 0;
line-height: 1.5rem;
pointer-events: none;
}
}
svg {
pointer-events: all;
& > polyline {
pointer-events: none;
}
}
.overlay-explorer {
padding: 0 0.5rem;
border: none;
& > #overlay-explorer-button {
background-color: var(--lightgray);
border: none;
border-radius: 4px;
font-family: inherit;
font-size: inherit;
height: 2rem;
padding: 0;
display: flex;
align-items: center;
text-align: inherit;
cursor: pointer;
white-space: nowrap;
width: 100%;
justify-content: space-between;
& > p {
display: inline;
padding: 0 1rem;
}
& svg {
width: 18px;
height: 18px;
min-width: 18px;
margin: 0 0.5rem;
}
}
& >#overlay-explorer-container {
position: fixed;
contain: layout;
z-index: 999;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
overflow-y: auto;
display: none;
backdrop-filter: blur(4px);
&.active {
display: inline-block;
}
& > #overlay-explorer-space {
width: 65%;
margin-top: 12vh;
margin-left: auto;
margin-right: auto;
@media all and (max-width: $pageWidth) {
max-width: 500px;
width: 90%;
}
& > * {
width: 100%;
border-radius: 7px;
background: var(--light);
box-shadow:
0 14px 50px rgba(27, 33, 48, 0.12),
0 10px 30px rgba(27, 33, 48, 0.16);
margin-bottom: 2em;
}
& > #overlay-explorer-content {
box-sizing: border-box;
padding: 0.5em 1em;
border: 1px solid var(--lightgray);
}
& ul {
list-style: none;
margin: 0.08rem 0;
padding: 0;
transition:
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a {
color: var(--dark);
opacity: 0.75;
pointer-events: all;
}
}
}
}
}

View File

@ -0,0 +1,6 @@
@use "../../styles/variables.scss" as *;
.pageicon {
width: 180;
height: 180;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,14 @@
.callout[data-callout="chassis"] {
---color: 255, 255, 255;
--callout-icon: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-frame"><line x1="22" x2="2" y1="6" y2="6"/><line x1="22" x2="2" y1="18" y2="18"/><line x1="6" x2="6" y1="2" y2="22"/><line x1="18" x2="18" y1="2" y2="22"/></svg>');
}
.callout[data-callout="system"] {
---color: 255, 255, 255;
--callout-icon: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pocket-knife"><path d="M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"/><path d="M18 6h.01"/><path d="M6 18h.01"/><path d="M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"/><path d="M18 11.66V22a4 4 0 0 0 4-4V6"/></svg>');
}
.callout[data-callout="module"] {
---color: 255, 255, 255;
--callout-icon: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circuit-board"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M11 9h4a2 2 0 0 0 2-2V3"/><circle cx="9" cy="9" r="2"/><path d="M7 21v-4a2 2 0 0 1 2-2h4"/><circle cx="15" cy="15" r="2"/></svg>');
}