md-explorer 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/public/app.js +100 -16
- package/server.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-explorer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Web-based file explorer for Markdown & HTML",
|
|
5
5
|
"bin": {
|
|
6
6
|
"md-explorer": "server.js"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/iamthemovie/md-explorer.git"
|
|
30
|
+
"url": "git+https://github.com/iamthemovie/md-explorer.git"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"express": "^4.21.0",
|
package/public/app.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
const sidebar = document.getElementById('sidebar');
|
|
6
6
|
let activeLabel = null;
|
|
7
7
|
|
|
8
|
+
// Map from file path to { label, node } for tree restoration
|
|
9
|
+
const treeNodes = {};
|
|
10
|
+
|
|
8
11
|
// --- Toggle sidebar ---
|
|
9
12
|
collapseBtn.addEventListener('click', () => {
|
|
10
13
|
sidebar.classList.toggle('collapsed');
|
|
@@ -43,17 +46,11 @@
|
|
|
43
46
|
childUl.className = 'tree-children';
|
|
44
47
|
let loaded = false;
|
|
45
48
|
|
|
49
|
+
// Store directory node for programmatic expansion
|
|
50
|
+
treeNodes[node.path] = { label, node, childUl, loaded: () => loaded, setLoaded: () => { loaded = true; } };
|
|
51
|
+
|
|
46
52
|
label.addEventListener('click', async () => {
|
|
47
|
-
|
|
48
|
-
if (opening && !loaded) {
|
|
49
|
-
const children = await fetchEntries(node.path);
|
|
50
|
-
const freshUl = buildTreeUl(children);
|
|
51
|
-
while (freshUl.firstChild) childUl.appendChild(freshUl.firstChild);
|
|
52
|
-
loaded = true;
|
|
53
|
-
}
|
|
54
|
-
childUl.classList.toggle('open');
|
|
55
|
-
label.querySelector('.tree-icon').innerHTML =
|
|
56
|
-
childUl.classList.contains('open') ? '▾' : '▸';
|
|
53
|
+
await expandDir(node.path);
|
|
57
54
|
});
|
|
58
55
|
|
|
59
56
|
li.appendChild(label);
|
|
@@ -67,6 +64,9 @@
|
|
|
67
64
|
'<span class="tree-icon">' + icon + '</span>' +
|
|
68
65
|
escapeHtml(node.name);
|
|
69
66
|
|
|
67
|
+
// Store file node for highlight restoration
|
|
68
|
+
treeNodes[node.path] = { label, node };
|
|
69
|
+
|
|
70
70
|
label.addEventListener('click', () => handleFileClick(node, label));
|
|
71
71
|
li.appendChild(label);
|
|
72
72
|
}
|
|
@@ -77,6 +77,52 @@
|
|
|
77
77
|
return ul;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Expand a directory in the tree (toggle or force open)
|
|
81
|
+
async function expandDir(dirPath, forceOpen) {
|
|
82
|
+
const entry = treeNodes[dirPath];
|
|
83
|
+
if (!entry || !entry.childUl) return;
|
|
84
|
+
|
|
85
|
+
const { label, childUl } = entry;
|
|
86
|
+
const isOpen = childUl.classList.contains('open');
|
|
87
|
+
|
|
88
|
+
if (forceOpen && isOpen) return; // already open
|
|
89
|
+
|
|
90
|
+
if (!isOpen && !entry.loaded()) {
|
|
91
|
+
const children = await fetchEntries(dirPath);
|
|
92
|
+
const freshUl = buildTreeUl(children);
|
|
93
|
+
while (freshUl.firstChild) childUl.appendChild(freshUl.firstChild);
|
|
94
|
+
entry.setLoaded();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!forceOpen || !isOpen) {
|
|
98
|
+
childUl.classList.toggle('open');
|
|
99
|
+
label.querySelector('.tree-icon').innerHTML =
|
|
100
|
+
childUl.classList.contains('open') ? '▾' : '▸';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expand all ancestor directories for a given file path and highlight it
|
|
105
|
+
async function expandToFile(filePath) {
|
|
106
|
+
const segments = filePath.split('/');
|
|
107
|
+
// Walk directory segments, expanding each
|
|
108
|
+
for (let i = 1; i < segments.length; i++) {
|
|
109
|
+
const dirPath = segments.slice(0, i).join('/');
|
|
110
|
+
// If the directory node isn't loaded in the tree yet, we need to wait
|
|
111
|
+
// for the parent to load first (which buildTreeUl handles via fetchEntries)
|
|
112
|
+
if (!treeNodes[dirPath]) break;
|
|
113
|
+
await expandDir(dirPath, true);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Highlight the file
|
|
117
|
+
const fileEntry = treeNodes[filePath];
|
|
118
|
+
if (fileEntry) {
|
|
119
|
+
if (activeLabel) activeLabel.classList.remove('active');
|
|
120
|
+
fileEntry.label.classList.add('active');
|
|
121
|
+
activeLabel = fileEntry.label;
|
|
122
|
+
fileEntry.label.scrollIntoView({ block: 'nearest' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
80
126
|
// --- File click handlers ---
|
|
81
127
|
function handleFileClick(node, label) {
|
|
82
128
|
const ext = node.name.split('.').pop().toLowerCase();
|
|
@@ -84,14 +130,20 @@
|
|
|
84
130
|
if (ext === 'html' || ext === 'htm') {
|
|
85
131
|
window.open('/raw/' + node.path, '_blank');
|
|
86
132
|
} else if (ext === 'md') {
|
|
87
|
-
loadMarkdown(node.path, label);
|
|
133
|
+
loadMarkdown(node.path, label, true);
|
|
88
134
|
}
|
|
89
135
|
}
|
|
90
136
|
|
|
91
|
-
async function loadMarkdown(filePath, label) {
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
async function loadMarkdown(filePath, label, pushHistory) {
|
|
138
|
+
if (label) {
|
|
139
|
+
if (activeLabel) activeLabel.classList.remove('active');
|
|
140
|
+
label.classList.add('active');
|
|
141
|
+
activeLabel = label;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (pushHistory) {
|
|
145
|
+
history.pushState(null, '', '/' + filePath);
|
|
146
|
+
}
|
|
95
147
|
|
|
96
148
|
try {
|
|
97
149
|
const res = await fetch('/api/file?path=' + encodeURIComponent(filePath));
|
|
@@ -107,6 +159,27 @@
|
|
|
107
159
|
}
|
|
108
160
|
}
|
|
109
161
|
|
|
162
|
+
// --- Back/forward navigation ---
|
|
163
|
+
window.addEventListener('popstate', () => {
|
|
164
|
+
const filePath = decodeURIComponent(location.pathname).replace(/^\//, '');
|
|
165
|
+
if (filePath) {
|
|
166
|
+
const entry = treeNodes[filePath];
|
|
167
|
+
const label = entry ? entry.label : null;
|
|
168
|
+
if (label) {
|
|
169
|
+
if (activeLabel) activeLabel.classList.remove('active');
|
|
170
|
+
label.classList.add('active');
|
|
171
|
+
activeLabel = label;
|
|
172
|
+
}
|
|
173
|
+
loadMarkdown(filePath, null, false);
|
|
174
|
+
} else {
|
|
175
|
+
// Navigated back to root
|
|
176
|
+
if (activeLabel) activeLabel.classList.remove('active');
|
|
177
|
+
activeLabel = null;
|
|
178
|
+
contentEl.innerHTML = '<p class="placeholder">Select a file from the sidebar</p>';
|
|
179
|
+
document.title = 'MarkdownX';
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
110
183
|
// --- Utilities ---
|
|
111
184
|
function escapeHtml(str) {
|
|
112
185
|
const el = document.createElement('span');
|
|
@@ -115,5 +188,16 @@
|
|
|
115
188
|
}
|
|
116
189
|
|
|
117
190
|
// --- Init ---
|
|
118
|
-
|
|
191
|
+
async function init() {
|
|
192
|
+
await loadTree();
|
|
193
|
+
|
|
194
|
+
// Restore file from URL if present
|
|
195
|
+
const filePath = decodeURIComponent(location.pathname).replace(/^\//, '');
|
|
196
|
+
if (filePath && filePath.toLowerCase().endsWith('.md')) {
|
|
197
|
+
await expandToFile(filePath);
|
|
198
|
+
loadMarkdown(filePath, null, false);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
init();
|
|
119
203
|
})();
|
package/server.js
CHANGED
|
@@ -179,6 +179,12 @@ app.get('/raw/*', (req, res) => {
|
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
// --- SPA catch-all (deep URL support) ---
|
|
183
|
+
|
|
184
|
+
app.get('*', (req, res) => {
|
|
185
|
+
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
186
|
+
});
|
|
187
|
+
|
|
182
188
|
// --- Start ---
|
|
183
189
|
|
|
184
190
|
const server = app.listen(PORT, () => {
|