-
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented OverlayExplorer for mobile view
- Loading branch information
Showing
5 changed files
with
502 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import OverlayExplorer from "./OverlayExplorer" | ||
|
||
export { | ||
OverlayExplorer, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
}) |
Oops, something went wrong.