mirror of
https://github.com/zen-browser/docs.git
synced 2025-07-07 17:05:34 +02:00
Implemented OverlayExplorer for mobile view
This commit is contained in:
parent
573537a102
commit
cbfb898c51
5 changed files with 502 additions and 0 deletions
|
@ -1,5 +1,6 @@
|
|||
import { PageLayout, SharedLayout } from "./quartz/cfg"
|
||||
import * as Component from "./quartz/components"
|
||||
import * as ExtraComponent from "./quartz/extra"
|
||||
|
||||
// components shared across all pages
|
||||
export const sharedPageComponents: SharedLayout = {
|
||||
|
@ -26,6 +27,9 @@ export const defaultContentPageLayout: PageLayout = {
|
|||
left: [
|
||||
Component.PageTitle(),
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.MobileOnly(ExtraComponent.OverlayExplorer(
|
||||
{filterFn: (node) => (node.name !== "tags" && node.name !== "License")},
|
||||
)),
|
||||
Component.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
|
@ -43,6 +47,9 @@ export const defaultListPageLayout: PageLayout = {
|
|||
left: [
|
||||
Component.PageTitle(),
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.MobileOnly(ExtraComponent.OverlayExplorer(
|
||||
{filterFn: (node) => (node.name !== "tags" && node.name !== "License")},
|
||||
)),
|
||||
Component.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
|
|
218
quartz/extra/OverlayExplorer.tsx
Normal file
218
quartz/extra/OverlayExplorer.tsx
Normal 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
|
5
quartz/extra/index.ts
Normal file
5
quartz/extra/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import OverlayExplorer from "./OverlayExplorer"
|
||||
|
||||
export {
|
||||
OverlayExplorer,
|
||||
}
|
108
quartz/extra/scripts/overlayexplorer.inline.ts
Normal file
108
quartz/extra/scripts/overlayexplorer.inline.ts
Normal 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()
|
||||
})
|
164
quartz/extra/styles/overlayexplorer.scss
Normal file
164
quartz/extra/styles/overlayexplorer.scss
Normal 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: $fullPageWidth) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue