mirror of
https://github.com/litruv/Docs-Viewer.git
synced 2026-07-24 10:46:09 +10:00
Implement folder structure in file index and enhance document loading logic
This commit is contained in:
156
index.js
156
index.js
@@ -23,54 +23,118 @@ async function initializeMarked() {
|
|||||||
return marked;
|
return marked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFileIndexItem(doc, container, level = 0) {
|
||||||
|
if (doc.type === 'folder') {
|
||||||
|
const folderDiv = document.createElement('div');
|
||||||
|
folderDiv.className = 'folder open';
|
||||||
|
folderDiv.dataset.path = doc.title; // Add data attribute for folder identification
|
||||||
|
folderDiv.style.paddingLeft = `${level * 0.8}rem`;
|
||||||
|
|
||||||
|
const folderHeader = document.createElement('div');
|
||||||
|
folderHeader.className = 'folder-header';
|
||||||
|
folderHeader.innerHTML = `
|
||||||
|
<i class="fas fa-folder-open folder-icon"></i>
|
||||||
|
<span>${doc.title}</span>
|
||||||
|
`;
|
||||||
|
folderDiv.appendChild(folderHeader);
|
||||||
|
|
||||||
|
const folderContent = document.createElement('div');
|
||||||
|
folderContent.className = 'folder-content';
|
||||||
|
doc.items.forEach(item => createFileIndexItem(item, folderContent, level + 1));
|
||||||
|
folderDiv.appendChild(folderContent);
|
||||||
|
|
||||||
|
folderHeader.addEventListener('click', () => {
|
||||||
|
folderDiv.classList.toggle('open');
|
||||||
|
folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-closed');
|
||||||
|
folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(folderDiv);
|
||||||
|
} else {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `?${doc.slug}`; // Use slug instead of generating filename
|
||||||
|
link.textContent = doc.title;
|
||||||
|
link.dataset.path = doc.path;
|
||||||
|
link.style.paddingLeft = `${level * 0.8 + 1.2}rem`;
|
||||||
|
|
||||||
|
link.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
loadDocument(doc.path);
|
||||||
|
history.pushState(null, '', link.href);
|
||||||
|
};
|
||||||
|
container.appendChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this new function to find parent folders of a path
|
||||||
|
function findParentFolders(documents, path, parentFolders = []) {
|
||||||
|
for (const doc of documents) {
|
||||||
|
if (doc.type === 'folder') {
|
||||||
|
// Check if path is in this folder
|
||||||
|
const found = doc.items.find(item => {
|
||||||
|
if (item.path === path) return true;
|
||||||
|
if (item.type === 'folder') {
|
||||||
|
return findParentFolders([item], path).length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
parentFolders.push(doc);
|
||||||
|
// Continue searching in case of nested folders
|
||||||
|
doc.items.forEach(item => {
|
||||||
|
if (item.type === 'folder') {
|
||||||
|
findParentFolders([item], path, parentFolders);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentFolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to find document by slug in nested structure
|
||||||
|
function findDocumentBySlug(documents, slug) {
|
||||||
|
for (const doc of documents) {
|
||||||
|
if (doc.type === 'folder') {
|
||||||
|
const found = findDocumentBySlug(doc.items, slug);
|
||||||
|
if (found) return found;
|
||||||
|
} else if (doc.slug === slug) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadIndex() {
|
async function loadIndex() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('index.json');
|
const response = await fetch('index.json');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const fileIndex = document.getElementById('file-index');
|
const fileIndex = document.getElementById('file-index');
|
||||||
|
|
||||||
// Build file index
|
// Clear existing content
|
||||||
data.documents.forEach(doc => {
|
fileIndex.innerHTML = '';
|
||||||
const link = document.createElement('a');
|
|
||||||
const fileName = doc.path.replace(/^docs\//, '')
|
// Build file index with folder support
|
||||||
.replace(/\.md$/, '')
|
data.documents.forEach(doc => createFileIndexItem(doc, fileIndex));
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
link.href = `?${fileName}`;
|
|
||||||
link.textContent = doc.title;
|
|
||||||
link.dataset.path = doc.path; // Add path as data attribute for comparison
|
|
||||||
link.onclick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
loadDocument(doc.path);
|
|
||||||
history.pushState(null, '', link.href);
|
|
||||||
};
|
|
||||||
fileIndex.appendChild(link);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle initial URL load or load index.md by default
|
// Handle initial URL load or load index.md by default
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
const fileParam = queryString.substring(1); // Remove the leading '?'
|
const slug = queryString.substring(1); // Remove the leading '?'
|
||||||
console.log('Loading document for:', fileParam); // Debug log
|
console.log('Loading document for slug:', slug); // Debug log
|
||||||
|
|
||||||
const matchingDoc = data.documents.find(doc => {
|
const matchingDoc = findDocumentBySlug(data.documents, slug);
|
||||||
const docFileName = doc.path.replace(/^docs\//, '')
|
|
||||||
.replace(/\.md$/, '')
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase();
|
|
||||||
return docFileName === fileParam;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchingDoc) {
|
if (matchingDoc) {
|
||||||
console.log('Found matching document:', matchingDoc); // Debug log
|
console.log('Found matching document:', matchingDoc); // Debug log
|
||||||
await loadDocument(matchingDoc.path);
|
await loadDocument(matchingDoc.path);
|
||||||
} else {
|
} else {
|
||||||
console.warn('No matching document found for:', fileParam); // Debug log
|
console.warn('No matching document found for slug:', slug); // Debug log
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load index.md by default when no document is specified
|
// Load index.md by default when no document is specified
|
||||||
const defaultDoc = data.documents.find(doc => doc.path === 'docs/index.md');
|
const defaultDoc = findDocumentBySlug(data.documents, 'welcome');
|
||||||
if (defaultDoc) {
|
if (defaultDoc) {
|
||||||
await loadDocument(defaultDoc.path);
|
await loadDocument(defaultDoc.path);
|
||||||
}
|
}
|
||||||
@@ -85,18 +149,11 @@ async function loadIndex() {
|
|||||||
// Add popstate handler for browser back/forward buttons
|
// Add popstate handler for browser back/forward buttons
|
||||||
window.addEventListener('popstate', async () => {
|
window.addEventListener('popstate', async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const fileParam = urlParams.toString().replace(/^=/, '').replace(/^\?/, '');
|
const slug = urlParams.toString().replace(/^=/, '').replace(/^\?/, '');
|
||||||
if (fileParam) {
|
if (slug) {
|
||||||
const response = await fetch('index.json');
|
const response = await fetch('index.json');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const matchingDoc = data.documents.find(doc => {
|
const matchingDoc = findDocumentBySlug(data.documents, slug);
|
||||||
const docFileName = doc.path.replace(/^docs\//, '')
|
|
||||||
.replace(/\.md$/, '')
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase();
|
|
||||||
return docFileName === fileParam;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchingDoc) {
|
if (matchingDoc) {
|
||||||
await loadDocument(matchingDoc.path);
|
await loadDocument(matchingDoc.path);
|
||||||
}
|
}
|
||||||
@@ -142,9 +199,28 @@ function extractMetadata(content) {
|
|||||||
|
|
||||||
async function loadDocument(path) {
|
async function loadDocument(path) {
|
||||||
try {
|
try {
|
||||||
// Update active state in sidebar
|
// Update active state in sidebar and expand folders
|
||||||
document.querySelectorAll('#file-index a').forEach(link => {
|
document.querySelectorAll('#file-index a').forEach(link => {
|
||||||
link.classList.toggle('active', link.dataset.path === path);
|
link.classList.toggle('active', link.dataset.path === path);
|
||||||
|
|
||||||
|
// If this is the active link, expand its parent folders
|
||||||
|
if (link.dataset.path === path) {
|
||||||
|
const response = fetch('index.json')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const parentFolders = findParentFolders(data.documents, path);
|
||||||
|
parentFolders.forEach(folder => {
|
||||||
|
// Find and expand the corresponding folder div
|
||||||
|
const folderDiv = document.querySelector(`.folder[data-path="${folder.title}"]`);
|
||||||
|
if (folderDiv) {
|
||||||
|
folderDiv.classList.add('open');
|
||||||
|
const icon = folderDiv.querySelector('.folder-icon');
|
||||||
|
icon.classList.remove('fa-folder-closed');
|
||||||
|
icon.classList.add('fa-folder-open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [response, marked] = await Promise.all([
|
const [response, marked] = await Promise.all([
|
||||||
|
|||||||
14
index.json
14
index.json
@@ -2,11 +2,19 @@
|
|||||||
"documents": [
|
"documents": [
|
||||||
{
|
{
|
||||||
"title": "Welcome",
|
"title": "Welcome",
|
||||||
"path": "docs/index.md"
|
"path": "docs/index.md",
|
||||||
|
"slug": "welcome"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Blueprint Depth Trace",
|
"title": "Unreal Engine",
|
||||||
"path": "docs/Blueprint Penetration Trace.md"
|
"type": "folder",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Blueprint Depth Trace",
|
||||||
|
"path": "docs/Blueprint Penetration Trace.md",
|
||||||
|
"slug": "blueprint-penetration-trace"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
35
styles.css
35
styles.css
@@ -102,6 +102,41 @@ body {
|
|||||||
border-left: 2px solid var(--ifm-color-primary);
|
border-left: 2px solid var(--ifm-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.folder {
|
||||||
|
color: var(--ifm-color-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.4rem 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-header:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
width: 1.2em;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder.open > .folder-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder .folder-header:hover .folder-icon {
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.social-links {
|
.social-links {
|
||||||
padding: 1rem 1rem 0.5rem;
|
padding: 1rem 1rem 0.5rem;
|
||||||
border-top: 1px solid var(--ifm-border-color);
|
border-top: 1px solid var(--ifm-border-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user