David Fischer
2023-09-19 cc31a40b0cb53cba7f51187cb6d68076c3f54c0f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { FolderState } from "../ExplorerNode"
 
// Current state of folders
let explorerState: FolderState[]
 
const observer = new IntersectionObserver((entries) => {
  // If last element is observed, remove gradient of "overflow" class so element is visible
  const explorer = document.getElementById("explorer-ul")
  for (const entry of entries) {
    if (entry.isIntersecting) {
      explorer?.classList.add("no-background")
    } else {
      explorer?.classList.remove("no-background")
    }
  }
})
 
function toggleExplorer(this: HTMLElement) {
  // Toggle collapsed state of entire explorer
  this.classList.toggle("collapsed")
  const content = this.nextElementSibling as HTMLElement
  content.classList.toggle("collapsed")
  content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
}
 
function toggleFolder(evt: MouseEvent) {
  evt.stopPropagation()
 
  // Element that was clicked
  const target = evt.target as HTMLElement
 
  // Check if target was svg icon or button
  const isSvg = target.nodeName === "svg"
 
  // corresponding <ul> element relative to clicked button/folder
  let childFolderContainer: HTMLElement
 
  // <li> element of folder (stores folder-path dataset)
  let currentFolderParent: HTMLElement
 
  // Get correct relative container and toggle collapsed class
  if (isSvg) {
    childFolderContainer = target.parentElement?.nextSibling as HTMLElement
    currentFolderParent = target.nextElementSibling as HTMLElement
 
    childFolderContainer.classList.toggle("open")
  } else {
    childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
    currentFolderParent = target.parentElement as HTMLElement
 
    childFolderContainer.classList.toggle("open")
  }
  if (!childFolderContainer) return
 
  // Collapse folder container
  const isCollapsed = childFolderContainer.classList.contains("open")
  setFolderState(childFolderContainer, !isCollapsed)
 
  // Save folder state to localStorage
  const clickFolderPath = currentFolderParent.dataset.folderpath as string
 
  // Remove leading "/"
  const fullFolderPath = clickFolderPath.substring(1)
  toggleCollapsedByPath(explorerState, fullFolderPath)
 
  const stringifiedFileTree = JSON.stringify(explorerState)
  localStorage.setItem("fileTree", stringifiedFileTree)
}
 
function setupExplorer() {
  // Set click handler for collapsing entire explorer
  const explorer = document.getElementById("explorer")
 
  // Get folder state from local storage
  const storageTree = localStorage.getItem("fileTree")
 
  // Convert to bool
  const useSavedFolderState = explorer?.dataset.savestate === "true"
 
  if (explorer) {
    // Get config
    const collapseBehavior = explorer.dataset.behavior
 
    // Add click handlers for all folders (click handler on folder "label")
    if (collapseBehavior === "collapse") {
      Array.prototype.forEach.call(
        document.getElementsByClassName("folder-button"),
        function (item) {
          item.removeEventListener("click", toggleFolder)
          item.addEventListener("click", toggleFolder)
        },
      )
    }
 
    // Add click handler to main explorer
    explorer.removeEventListener("click", toggleExplorer)
    explorer.addEventListener("click", toggleExplorer)
  }
 
  // Set up click handlers for each folder (click handler on folder "icon")
  Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
    item.removeEventListener("click", toggleFolder)
    item.addEventListener("click", toggleFolder)
  })
 
  if (storageTree && useSavedFolderState) {
    // Get state from localStorage and set folder state
    explorerState = JSON.parse(storageTree)
    explorerState.map((folderUl) => {
      // grab <li> element for matching folder path
      const folderLi = document.querySelector(
        `[data-folderpath='/${folderUl.path}']`,
      ) as HTMLElement
 
      // Get corresponding content <ul> tag and set state
      const folderUL = folderLi.parentElement?.nextElementSibling
      if (folderUL) {
        setFolderState(folderUL as HTMLElement, folderUl.collapsed)
      }
    })
  } else {
    // If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
    explorerState = JSON.parse(explorer?.dataset.tree as string)
  }
}
 
window.addEventListener("resize", setupExplorer)
document.addEventListener("nav", () => {
  setupExplorer()
 
  const explorerContent = document.getElementById("explorer-ul")
  // select pseudo element at end of list
  const lastItem = document.getElementById("explorer-end")
 
  observer.disconnect()
  observer.observe(lastItem as Element)
})
 
/**
 * Toggles the state of a given folder
 * @param folderElement <div class="folder-outer"> Element of folder (parent)
 * @param collapsed if folder should be set to collapsed or not
 */
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
  if (collapsed) {
    folderElement?.classList.remove("open")
  } else {
    folderElement?.classList.add("open")
  }
}
 
/**
 * Toggles visibility of a folder
 * @param array array of FolderState (`fileTree`, either get from local storage or data attribute)
 * @param path path to folder (e.g. 'advanced/more/more2')
 */
function toggleCollapsedByPath(array: FolderState[], path: string) {
  const entry = array.find((item) => item.path === path)
  if (entry) {
    entry.collapsed = !entry.collapsed
  }
}