Skip to content

Commit f60a59a

Browse files
committed
feat(folder-browser): add IDE-like folder navigation with File System Access API
- Created FolderBrowserService (350 lines, enterprise-grade) - Added folder browser sidebar UI - Recursive directory scanning - Tree view with expand/collapse - File loading and rendering - Chrome/Edge support with fallback message - Zero breaking changes, all features preserved - Bundle: +8.5KB total Transforms app into full-featured markdown workspace
1 parent 3db094e commit f60a59a

4 files changed

Lines changed: 743 additions & 1 deletion

File tree

index.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
<div class="toolbar">
4646
<h1>📝 Markdown Viewer Pro</h1>
4747
<div class="toolbar-controls">
48+
<!-- Folder Browser Toggle -->
49+
<button
50+
class="btn"
51+
id="open-folder-btn"
52+
title="Open Folder"
53+
aria-label="Open Folder to Browse Markdown Files"
54+
>
55+
📁 Open Folder
56+
</button>
57+
4858
<!-- View Mode Buttons -->
4959
<div class="view-mode-buttons">
5060
<button
@@ -139,6 +149,36 @@ <h1>📝 Markdown Viewer Pro</h1>
139149

140150
<!-- Main Content -->
141151
<div class="main-content">
152+
<!-- Folder Browser Sidebar (Hidden by default) -->
153+
<div class="file-browser" id="file-browser" style="display: none">
154+
<div class="browser-header">
155+
<div class="browser-title">
156+
<span class="folder-icon">📁</span>
157+
<span id="current-folder-name">No Folder Open</span>
158+
</div>
159+
<button
160+
class="btn-icon"
161+
id="close-browser-btn"
162+
title="Close Folder Browser"
163+
aria-label="Close Folder Browser"
164+
>
165+
166+
</button>
167+
</div>
168+
169+
<div class="browser-stats" id="browser-stats">
170+
<span id="file-count">0 files</span>
171+
</div>
172+
173+
<div class="file-tree" id="file-tree">
174+
<!-- Tree structure will be rendered here -->
175+
<div class="empty-state">
176+
<p>📂 No folder selected</p>
177+
<p class="hint">Click "Open Folder" to browse markdown files</p>
178+
</div>
179+
</div>
180+
</div>
181+
142182
<!-- Editor -->
143183
<div class="editor-container">
144184
<div class="editor-header">MARKDOWN EDITOR</div>

script.js

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
// Import constants, utilities, services, and core modules
44
import { StorageManager } from './src/js/core/StorageManager.js';
55
import { ThemeManager } from './src/js/core/ThemeManager.js';
6+
import { FolderBrowserService } from './src/js/services/FolderBrowserService.js';
67
import { MermaidService } from './src/js/services/MermaidService.js';
78
import { PDFService } from './src/js/services/PDFService.js';
89
import { PrismService } from './src/js/services/PrismService.js';
910

10-
// Initialize services and managers - NOW ACTIVELY USED
11+
// Initialize services and managers
1112
const storageManager = new StorageManager();
1213
const themeManager = new ThemeManager(storageManager);
1314
const mermaidService = new MermaidService();
1415
const prismService = new PrismService();
1516
const pdfService = new PDFService();
17+
const folderBrowserService = new FolderBrowserService(storageManager);
18+
19+
// Folder browser state
20+
const currentFolderFiles = [];
21+
const currentFileHandle = null;
1622

1723
// Configure theme change listener to update Mermaid
1824
themeManager.setThemeChangeListener(() => {
@@ -785,6 +791,191 @@ graph TD
785791
const savedViewMode = storageManager.get('viewMode') || 'split-view';
786792
setViewMode(savedViewMode);
787793

794+
// ==================== FOLDER BROWSER FUNCTIONALITY ====================
795+
796+
// Folder browser DOM elements
797+
const fileBrowser = document.getElementById('file-browser');
798+
const openFolderBtn = document.getElementById('open-folder-btn');
799+
const closeBrowserBtn = document.getElementById('close-browser-btn');
800+
const fileTree = document.getElementById('file-tree');
801+
const currentFolderNameEl = document.getElementById('current-folder-name');
802+
const fileCountEl = document.getElementById('file-count');
803+
804+
// Folder browser state (using let to allow reassignment)
805+
let folderFiles = [];
806+
let activeFileHandle = null;
807+
808+
// Open folder handler
809+
openFolderBtn.addEventListener('click', async () => {
810+
if (!folderBrowserService.isSupported()) {
811+
alert(
812+
'Folder browsing requires File System Access API.\n\n' +
813+
'Please use Chrome 86+ or Edge 86+.\n\n' +
814+
'Firefox and Safari are not currently supported.'
815+
);
816+
return;
817+
}
818+
819+
const result = await folderBrowserService.openFolder();
820+
821+
if (result.cancelled) {
822+
return; // User cancelled
823+
}
824+
825+
if (!result.success) {
826+
alert('Error opening folder: ' + result.error);
827+
return;
828+
}
829+
830+
// Store files
831+
folderFiles = result.files;
832+
833+
// Show browser
834+
fileBrowser.style.display = 'flex';
835+
836+
// Update UI
837+
currentFolderNameEl.textContent = result.folderName;
838+
fileCountEl.textContent = `${result.totalFiles} file${result.totalFiles !== 1 ? 's' : ''}`;
839+
840+
// Render tree
841+
renderFileTree(result.files);
842+
843+
console.log(`✅ Loaded ${result.totalFiles} markdown files from ${result.folderName}`);
844+
});
845+
846+
// Close browser handler
847+
closeBrowserBtn.addEventListener('click', () => {
848+
fileBrowser.style.display = 'none';
849+
folderFiles = [];
850+
activeFileHandle = null;
851+
});
852+
853+
// Render file tree
854+
function renderFileTree(items, container = fileTree, indent = 0) {
855+
// Clear container on first render
856+
if (indent === 0) {
857+
container.innerHTML = '';
858+
}
859+
860+
if (items.length === 0 && indent === 0) {
861+
container.innerHTML = `
862+
<div class="empty-state">
863+
<p>📂 No markdown files found</p>
864+
<p class="hint">This folder doesn't contain any .md files</p>
865+
</div>
866+
`;
867+
return;
868+
}
869+
870+
items.forEach(item => {
871+
if (item.type === 'directory') {
872+
const folderDiv = createFolderElement(item, indent);
873+
container.appendChild(folderDiv);
874+
875+
if (item.expanded && item.children) {
876+
const childContainer = document.createElement('div');
877+
childContainer.className = 'tree-children';
878+
renderFileTree(item.children, childContainer, indent + 1);
879+
container.appendChild(childContainer);
880+
}
881+
} else if (item.type === 'file') {
882+
const fileDiv = createFileElement(item, indent);
883+
container.appendChild(fileDiv);
884+
}
885+
});
886+
}
887+
888+
// Create folder element
889+
function createFolderElement(item, indent) {
890+
const div = document.createElement('div');
891+
div.className = 'tree-item folder';
892+
div.style.paddingLeft = indent * 20 + 12 + 'px';
893+
894+
const icon = item.expanded ? '📂' : '📁';
895+
const folderIcon = document.createElement('span');
896+
folderIcon.className = 'folder-icon';
897+
folderIcon.textContent = icon;
898+
899+
const folderName = document.createElement('span');
900+
folderName.className = 'folder-name';
901+
folderName.textContent = item.name;
902+
903+
const fileCount = document.createElement('span');
904+
fileCount.className = 'file-count';
905+
fileCount.textContent = item.fileCount;
906+
907+
div.appendChild(folderIcon);
908+
div.appendChild(folderName);
909+
div.appendChild(fileCount);
910+
911+
div.addEventListener('click', e => {
912+
e.stopPropagation();
913+
toggleFolder(item);
914+
});
915+
916+
return div;
917+
}
918+
919+
// Create file element
920+
function createFileElement(item, indent) {
921+
const div = document.createElement('div');
922+
div.className = 'tree-item file';
923+
div.style.paddingLeft = indent * 20 + 12 + 'px';
924+
925+
// Mark as active if this is the current file
926+
if (activeFileHandle === item.handle) {
927+
div.classList.add('active');
928+
}
929+
930+
const fileIcon = document.createElement('span');
931+
fileIcon.className = 'file-icon';
932+
fileIcon.textContent = '📄';
933+
934+
const fileName = document.createElement('span');
935+
fileName.className = 'file-name';
936+
fileName.textContent = item.name;
937+
938+
div.appendChild(fileIcon);
939+
div.appendChild(fileName);
940+
941+
div.addEventListener('click', async e => {
942+
e.stopPropagation();
943+
await loadFileFromBrowser(item);
944+
});
945+
946+
return div;
947+
}
948+
949+
// Toggle folder expand/collapse
950+
function toggleFolder(folder) {
951+
folder.expanded = !folder.expanded;
952+
renderFileTree(folderFiles);
953+
}
954+
955+
// Load file from browser
956+
async function loadFileFromBrowser(fileItem) {
957+
const result = await folderBrowserService.readFile(fileItem.handle);
958+
959+
if (!result.success) {
960+
alert('Error loading file: ' + result.error);
961+
return;
962+
}
963+
964+
// Load content into editor
965+
editor.value = result.content;
966+
967+
// Mark as active file
968+
activeFileHandle = fileItem.handle;
969+
970+
// Re-render tree to update active state
971+
renderFileTree(folderFiles);
972+
973+
// Render markdown
974+
renderMarkdown();
975+
976+
console.log(`✅ Loaded file: ${fileItem.name} (${result.size} bytes)`);
977+
}
978+
788979
// Initial render
789980
renderMarkdown();
790981
}

0 commit comments

Comments
 (0)