Wanted a drop down menu for my dashboard so I used chat GPT to create this query and a css snippet to style it. I’m sure the query could be simplified or there is a better way to do this but it works and links to the pages AND it’s all updated automatically.
DataviewJS Query
function formatDate(dateString) {
let date = new Date(dateString);
let options = { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true };
return date.toLocaleString("en-US", options);
}
let folderTree = new Map();
function addToFolderTree(path, content) {
let parts = path.split("/");
let currentLevel = folderTree;
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (!currentLevel.has(part)) {
currentLevel.set(part, { folders: new Map(), files: "" });
}
if (i === parts.length - 1) {
currentLevel.get(part).files = content;
} else {
currentLevel = currentLevel.get(part).folders;
}
}
}
for (let group of dv.pages('"----01. PROJECTS"').groupBy(p => p.file.folder)) {
let folderName = group.key;
let rows = group.rows
.map(
k => `| [${k.file.name.replace(/\|/g, "\\|")}](/${k.file.path.replace(/ /g, "%20")}) | ${formatDate(k.file.ctime)} | ${formatDate(k.file.mtime)} |`
)
.join("\n");
let tableContent = `
| Name | Created | Modified |
| ---- | ------- | -------- |
${rows}
`;
addToFolderTree(folderName, tableContent);
}
function renderFolderTree(folderMap, level = 0) {
let content = "";
for (let [folder, data] of folderMap.entries()) {
let subcontent = data.folders.size > 0 ? renderFolderTree(data.folders, level + 1) : "";
let folderContent = data.files ? data.files : "";
if (level > 0) {
content += `### ${folder}\n<details><summary>Click to expand</summary>\n\n${subcontent}${folderContent}\n\n</details>\n`;
} else {
content += `${subcontent}${folderContent}`;
}
}
return content;
}
dv.header(6, renderFolderTree(folderTree));
CSS
details {
font: 16px "Open Sans", Calibri, sans-serif;
width: 620px;
}
details > summary {
padding: 2px 6px;
background-color: #424741;
border: none;
box-shadow: 3px 3px 4px black;
cursor: pointer;
list-style: none;
}
details > p {
border-radius: 0 0 10px 10px;
background-color: #ddd;
padding: 2px 6px;
margin: 0;
box-shadow: 3px 3px 4px black;
}
details:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
}
details summary::-webkit-details-marker {
color: transparent;
transform: scale(0.01);
}
details summary::before {
content: '🡵';
display: inline-block;
font-size: 0.9em;
margin-right: 8px;
transform: translateY(-1px);
}
details[open] {
background-color: var(--background-secondary);
}
details[open] summary {
margin-bottom: 8px;
}
details[open] summary::before {
transform: translateY(-1px) rotate(90deg);
}
details[open] summary:before {
content: '';
position: absolute;
top: 100%;
left: 50%;
width: 0;
height: 0;
margin-left: -8px;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #ccc;
}
summary::marker {
content: "";
}
details > * {
padding-left: 12px;
padding-right: 12px;
}
details summary:hover::before {
content: "";
position: absolute;
z-index: 999999;
top: 0;
right: 0;
bottom: 0;
left: 0;
animation: bg-move 2s linear infinite;
background-size: 100% 8px;
background-image: linear-gradient(0, rgba(255, 255, 255, 0.05) 10%, transparent 10%, transparent 50%, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.05) 60%, transparent 60%, transparent);
}
@keyframes bg-move {
0% {
background-position: 0 0;
}
100% {
background-position: 0 -32px;
}
}
.heading-collapse-indicator,
.cm-fold-indicator .collapse-indicator {
opacity: 0 !important;
}
details summary::-webkit-details-marker {
font-size: 0.001px;
}
details > summary {
list-style: none;
}