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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/public/app.js +100 -16
  3. package/server.js +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-explorer",
3
- "version": "1.0.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
- const opening = !childUl.classList.contains('open');
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') ? '&#9662;' : '&#9656;';
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 (activeLabel) activeLabel.classList.remove('active');
93
- label.classList.add('active');
94
- activeLabel = label;
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
- loadTree();
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, () => {