This commit is contained in:
Alexei Chernyavsky 2025-12-14 06:17:25 +03:00 committed by GitHub
commit 9518117311
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 19 additions and 7 deletions

View File

@ -26,6 +26,7 @@ Component.Explorer({
title: "Explorer", // title of the explorer component title: "Explorer", // title of the explorer component
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click) folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open") folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
maxExpandLevel: 0, // number of folder levels expanded initially when folders are open; 0 means no limit (all levels expanded)
useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer
// omitted but shown later // omitted but shown later
sortFn: ..., sortFn: ...,
@ -36,6 +37,9 @@ Component.Explorer({
}) })
``` ```
- When `folderDefaultState` is `"collapsed"`, folders are collapsed by default, and `maxExpandLevel` does not affect initial folder expansion.
- When `folderDefaultState` is `"open"`, `maxExpandLevel` controls how many folder levels are expanded initially.
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
Want to customize it even more? Want to customize it even more?

View File

@ -14,6 +14,7 @@ type OrderEntries = "sort" | "filter" | "map"
export interface Options { export interface Options {
title?: string title?: string
folderDefaultState: "collapsed" | "open" folderDefaultState: "collapsed" | "open"
maxExpandLevel: number
folderClickBehavior: "collapse" | "link" folderClickBehavior: "collapse" | "link"
useSavedState: boolean useSavedState: boolean
sortFn: (a: FileTrieNode, b: FileTrieNode) => number sortFn: (a: FileTrieNode, b: FileTrieNode) => number
@ -24,6 +25,7 @@ export interface Options {
const defaultOptions: Options = { const defaultOptions: Options = {
folderDefaultState: "collapsed", folderDefaultState: "collapsed",
maxExpandLevel: 0,
folderClickBehavior: "link", folderClickBehavior: "link",
useSavedState: true, useSavedState: true,
mapFn: (node) => { mapFn: (node) => {
@ -68,6 +70,7 @@ export default ((userOpts?: Partial<Options>) => {
class={classNames(displayClass, "explorer")} class={classNames(displayClass, "explorer")}
data-behavior={opts.folderClickBehavior} data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState} data-collapsed={opts.folderDefaultState}
data-maxexpandlevel={opts.maxExpandLevel}
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-data-fns={JSON.stringify({ data-data-fns={JSON.stringify({
order: opts.order, order: opts.order,

View File

@ -7,6 +7,7 @@ type MaybeHTMLElement = HTMLElement | undefined
interface ParsedOptions { interface ParsedOptions {
folderClickBehavior: "collapse" | "link" folderClickBehavior: "collapse" | "link"
folderDefaultState: "collapsed" | "open" folderDefaultState: "collapsed" | "open"
maxExpandLevel: number
useSavedState: boolean useSavedState: boolean
sortFn: (a: FileTrieNode, b: FileTrieNode) => number sortFn: (a: FileTrieNode, b: FileTrieNode) => number
filterFn: (node: FileTrieNode) => boolean filterFn: (node: FileTrieNode) => boolean
@ -99,6 +100,7 @@ function createFolderNode(
currentSlug: FullSlug, currentSlug: FullSlug,
node: FileTrieNode, node: FileTrieNode,
opts: ParsedOptions, opts: ParsedOptions,
level: number = 1,
): HTMLLIElement { ): HTMLLIElement {
const template = document.getElementById("template-folder") as HTMLTemplateElement const template = document.getElementById("template-folder") as HTMLTemplateElement
const clone = template.content.cloneNode(true) as DocumentFragment const clone = template.content.cloneNode(true) as DocumentFragment
@ -125,10 +127,8 @@ function createFolderNode(
span.textContent = node.displayName span.textContent = node.displayName
} }
// if the saved state is collapsed or the default state is collapsed const savedState = currentExplorerState.find((item) => item.path === folderPath)?.collapsed
const isCollapsed = const isCollapsedByState = savedState ?? opts.folderDefaultState === "collapsed"
currentExplorerState.find((item) => item.path === folderPath)?.collapsed ??
opts.folderDefaultState === "collapsed"
// if this folder is a prefix of the current path we // if this folder is a prefix of the current path we
// want to open it anyways // want to open it anyways
@ -136,13 +136,17 @@ function createFolderNode(
const folderIsPrefixOfCurrentSlug = const folderIsPrefixOfCurrentSlug =
simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length) simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length)
if (!isCollapsed || folderIsPrefixOfCurrentSlug) { const maxLevel = opts.maxExpandLevel ?? 0
if (!isCollapsedByState || folderIsPrefixOfCurrentSlug || level <= maxLevel) {
folderOuter.classList.add("open") folderOuter.classList.add("open")
} else {
folderOuter.classList.remove("open")
} }
for (const child of node.children) { for (const child of node.children) {
const childNode = child.isFolder const childNode = child.isFolder
? createFolderNode(currentSlug, child, opts) ? createFolderNode(currentSlug, child, opts, level + 1)
: createFileNode(currentSlug, child) : createFileNode(currentSlug, child)
ul.appendChild(childNode) ul.appendChild(childNode)
} }
@ -158,6 +162,7 @@ async function setupExplorer(currentSlug: FullSlug) {
const opts: ParsedOptions = { const opts: ParsedOptions = {
folderClickBehavior: (explorer.dataset.behavior || "collapse") as "collapse" | "link", folderClickBehavior: (explorer.dataset.behavior || "collapse") as "collapse" | "link",
folderDefaultState: (explorer.dataset.collapsed || "collapsed") as "collapsed" | "open", folderDefaultState: (explorer.dataset.collapsed || "collapsed") as "collapsed" | "open",
maxExpandLevel: parseInt(explorer.dataset.maxexpandlevel || "0"),
useSavedState: explorer.dataset.savestate === "true", useSavedState: explorer.dataset.savestate === "true",
order: dataFns.order || ["filter", "map", "sort"], order: dataFns.order || ["filter", "map", "sort"],
sortFn: new Function("return " + (dataFns.sortFn || "undefined"))(), sortFn: new Function("return " + (dataFns.sortFn || "undefined"))(),
@ -209,7 +214,7 @@ async function setupExplorer(currentSlug: FullSlug) {
const fragment = document.createDocumentFragment() const fragment = document.createDocumentFragment()
for (const child of trie.children) { for (const child of trie.children) {
const node = child.isFolder const node = child.isFolder
? createFolderNode(currentSlug, child, opts) ? createFolderNode(currentSlug, child, opts, 1)
: createFileNode(currentSlug, child) : createFileNode(currentSlug, child)
fragment.appendChild(node) fragment.appendChild(node)