markdown-notes-engine 1.0.0
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/README.md +196 -0
- package/lib/README.md +312 -0
- package/lib/backend/github.js +318 -0
- package/lib/backend/index.js +76 -0
- package/lib/backend/markdown.js +62 -0
- package/lib/backend/routes/notes.js +197 -0
- package/lib/backend/routes/search.js +28 -0
- package/lib/backend/routes/upload.js +122 -0
- package/lib/backend/storage.js +121 -0
- package/lib/frontend/index.js +665 -0
- package/lib/frontend/styles.css +431 -0
- package/lib/index.js +28 -0
- package/package.json +51 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Notes Engine - Frontend Module
|
|
3
|
+
*
|
|
4
|
+
* A markdown note-taking editor with GitHub integration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class NotesEditor {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = {
|
|
10
|
+
apiEndpoint: config.apiEndpoint || '/api',
|
|
11
|
+
container: config.container,
|
|
12
|
+
theme: config.theme || 'light',
|
|
13
|
+
autoSave: config.autoSave !== false,
|
|
14
|
+
autoSaveDelay: config.autoSaveDelay || 2000,
|
|
15
|
+
onSave: config.onSave || null,
|
|
16
|
+
onLoad: config.onLoad || null,
|
|
17
|
+
onError: config.onError || null
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Check localStorage for saved dark mode preference
|
|
21
|
+
const savedDarkMode = localStorage.getItem('notesDarkMode');
|
|
22
|
+
const isDarkMode = savedDarkMode === 'enabled' || (savedDarkMode === null && config.theme === 'dark');
|
|
23
|
+
|
|
24
|
+
this.state = {
|
|
25
|
+
currentNote: null,
|
|
26
|
+
previewMode: 'editor', // 'editor', 'split', 'preview'
|
|
27
|
+
structure: [],
|
|
28
|
+
isDarkMode: isDarkMode
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.timers = {
|
|
32
|
+
autoSave: null,
|
|
33
|
+
previewUpdate: null
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.init();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the editor
|
|
41
|
+
*/
|
|
42
|
+
init() {
|
|
43
|
+
const container = document.querySelector(this.config.container);
|
|
44
|
+
if (!container) {
|
|
45
|
+
throw new Error(`Container "${this.config.container}" not found`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Inject HTML structure
|
|
49
|
+
container.innerHTML = this.getTemplate();
|
|
50
|
+
|
|
51
|
+
// Cache DOM elements
|
|
52
|
+
this.elements = {
|
|
53
|
+
sidebar: container.querySelector('.notes-sidebar'),
|
|
54
|
+
editor: container.querySelector('.notes-editor'),
|
|
55
|
+
preview: container.querySelector('.notes-preview'),
|
|
56
|
+
fileTree: container.querySelector('.notes-file-tree'),
|
|
57
|
+
notePath: container.querySelector('.notes-path-input'),
|
|
58
|
+
editorTextarea: container.querySelector('.notes-editor-textarea'),
|
|
59
|
+
previewContent: container.querySelector('.notes-preview-content'),
|
|
60
|
+
searchInput: container.querySelector('.notes-search-input')
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Apply theme
|
|
64
|
+
if (this.state.isDarkMode) {
|
|
65
|
+
container.classList.add('notes-dark-mode');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update theme button icon
|
|
69
|
+
const themeBtn = container.querySelector('[data-action="theme"]');
|
|
70
|
+
if (themeBtn) {
|
|
71
|
+
themeBtn.textContent = this.state.isDarkMode ? '☀️' : '🌙';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Setup event listeners
|
|
75
|
+
this.setupEventListeners();
|
|
76
|
+
|
|
77
|
+
// Load structure
|
|
78
|
+
this.loadStructure();
|
|
79
|
+
|
|
80
|
+
// Load marked.js and highlight.js from CDN
|
|
81
|
+
this.loadDependencies();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get HTML template
|
|
86
|
+
*/
|
|
87
|
+
getTemplate() {
|
|
88
|
+
return `
|
|
89
|
+
<div class="notes-container">
|
|
90
|
+
<div class="notes-sidebar">
|
|
91
|
+
<div class="notes-toolbar">
|
|
92
|
+
<button class="notes-btn" data-action="new">+ New</button>
|
|
93
|
+
<button class="notes-btn" data-action="folder">+ Folder</button>
|
|
94
|
+
<button class="notes-btn" data-action="theme">🌙</button>
|
|
95
|
+
</div>
|
|
96
|
+
<input type="text" class="notes-search-input" placeholder="Search notes...">
|
|
97
|
+
<div class="notes-file-tree"></div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="notes-main">
|
|
100
|
+
<div class="notes-header">
|
|
101
|
+
<input type="text" class="notes-path-input" placeholder="Note path (e.g., my-note)">
|
|
102
|
+
<div class="notes-actions">
|
|
103
|
+
<button class="notes-btn" data-action="save" title="Save (Ctrl+S)">💾 Save</button>
|
|
104
|
+
<button class="notes-btn" data-action="preview" title="Toggle Preview (Ctrl+P)">👁️ Preview</button>
|
|
105
|
+
<button class="notes-btn" data-action="upload-image" title="Upload Image (Ctrl+U)">🖼️</button>
|
|
106
|
+
<button class="notes-btn" data-action="upload-video" title="Upload Video (Ctrl+Shift+U)">🎥</button>
|
|
107
|
+
<button class="notes-btn notes-btn-danger" data-action="delete">🗑️</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="notes-content">
|
|
111
|
+
<div class="notes-editor">
|
|
112
|
+
<textarea class="notes-editor-textarea" placeholder="Start writing..."></textarea>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="notes-preview" style="display: none;">
|
|
115
|
+
<div class="notes-preview-content"></div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Setup event listeners
|
|
125
|
+
*/
|
|
126
|
+
setupEventListeners() {
|
|
127
|
+
const container = document.querySelector(this.config.container);
|
|
128
|
+
|
|
129
|
+
// Button actions
|
|
130
|
+
container.addEventListener('click', (e) => {
|
|
131
|
+
const btn = e.target.closest('[data-action]');
|
|
132
|
+
if (!btn) return;
|
|
133
|
+
|
|
134
|
+
const action = btn.dataset.action;
|
|
135
|
+
const actions = {
|
|
136
|
+
'new': () => this.createNewNote(),
|
|
137
|
+
'save': () => this.saveNote(),
|
|
138
|
+
'delete': () => this.deleteNote(),
|
|
139
|
+
'preview': () => this.togglePreview(),
|
|
140
|
+
'folder': () => this.createFolder(),
|
|
141
|
+
'upload-image': () => this.uploadImage(),
|
|
142
|
+
'upload-video': () => this.uploadVideo(),
|
|
143
|
+
'theme': () => this.toggleTheme()
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (actions[action]) {
|
|
147
|
+
actions[action]();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Auto-save
|
|
152
|
+
if (this.config.autoSave) {
|
|
153
|
+
this.elements.editorTextarea.addEventListener('input', () => {
|
|
154
|
+
clearTimeout(this.timers.autoSave);
|
|
155
|
+
this.timers.autoSave = setTimeout(() => {
|
|
156
|
+
if (this.state.currentNote) {
|
|
157
|
+
this.saveNote(true);
|
|
158
|
+
}
|
|
159
|
+
}, this.config.autoSaveDelay);
|
|
160
|
+
|
|
161
|
+
// Update preview
|
|
162
|
+
if (this.state.previewMode !== 'editor') {
|
|
163
|
+
clearTimeout(this.timers.previewUpdate);
|
|
164
|
+
this.timers.previewUpdate = setTimeout(() => {
|
|
165
|
+
this.renderPreview();
|
|
166
|
+
}, 100);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Keyboard shortcuts
|
|
172
|
+
this.elements.editorTextarea.addEventListener('keydown', (e) => {
|
|
173
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
this.saveNote();
|
|
176
|
+
} else if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
this.togglePreview();
|
|
179
|
+
} else if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
this.createNewNote();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Drag and drop
|
|
186
|
+
this.elements.editorTextarea.addEventListener('drop', async (e) => {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
const files = Array.from(e.dataTransfer.files);
|
|
189
|
+
for (const file of files) {
|
|
190
|
+
if (file.type.startsWith('image/')) {
|
|
191
|
+
await this.uploadImageFile(file);
|
|
192
|
+
} else if (file.type.startsWith('video/')) {
|
|
193
|
+
await this.uploadVideoFile(file);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.elements.editorTextarea.addEventListener('dragover', (e) => {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Paste images
|
|
203
|
+
this.elements.editorTextarea.addEventListener('paste', async (e) => {
|
|
204
|
+
const items = Array.from(e.clipboardData.items);
|
|
205
|
+
for (const item of items) {
|
|
206
|
+
if (item.type.startsWith('image/')) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
const file = item.getAsFile();
|
|
209
|
+
if (file) await this.uploadImageFile(file);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load external dependencies
|
|
217
|
+
*/
|
|
218
|
+
loadDependencies() {
|
|
219
|
+
if (!window.marked) {
|
|
220
|
+
const script = document.createElement('script');
|
|
221
|
+
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
|
222
|
+
document.head.appendChild(script);
|
|
223
|
+
}
|
|
224
|
+
if (!window.hljs) {
|
|
225
|
+
const script = document.createElement('script');
|
|
226
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
|
|
227
|
+
document.head.appendChild(script);
|
|
228
|
+
|
|
229
|
+
// Load both light and dark themes
|
|
230
|
+
const lightTheme = document.createElement('link');
|
|
231
|
+
lightTheme.rel = 'stylesheet';
|
|
232
|
+
lightTheme.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
|
|
233
|
+
lightTheme.id = 'hljs-light-theme';
|
|
234
|
+
lightTheme.disabled = this.state.isDarkMode; // Disable if dark mode is active
|
|
235
|
+
document.head.appendChild(lightTheme);
|
|
236
|
+
|
|
237
|
+
const darkTheme = document.createElement('link');
|
|
238
|
+
darkTheme.rel = 'stylesheet';
|
|
239
|
+
darkTheme.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css';
|
|
240
|
+
darkTheme.id = 'hljs-dark-theme';
|
|
241
|
+
darkTheme.disabled = !this.state.isDarkMode; // Disable if light mode is active
|
|
242
|
+
document.head.appendChild(darkTheme);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* API Methods
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
async loadStructure() {
|
|
251
|
+
try {
|
|
252
|
+
const response = await fetch(`${this.config.apiEndpoint}/structure`);
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
this.state.structure = data.structure || [];
|
|
255
|
+
this.renderFileTree();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.state.structure = [];
|
|
258
|
+
this.handleError('Failed to load file structure', error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async loadNote(path) {
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${this.config.apiEndpoint}/note?path=${encodeURIComponent(path)}`);
|
|
265
|
+
const data = await response.json();
|
|
266
|
+
|
|
267
|
+
this.state.currentNote = path;
|
|
268
|
+
this.elements.notePath.value = path;
|
|
269
|
+
this.elements.editorTextarea.value = data.content;
|
|
270
|
+
|
|
271
|
+
if (this.state.previewMode !== 'editor') {
|
|
272
|
+
this.renderPreview();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.config.onLoad) {
|
|
276
|
+
this.config.onLoad(path, data.content);
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.handleError('Failed to load note', error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async saveNote(silent = false) {
|
|
284
|
+
const path = this.elements.notePath.value.trim();
|
|
285
|
+
const content = this.elements.editorTextarea.value;
|
|
286
|
+
|
|
287
|
+
if (!path) {
|
|
288
|
+
if (!silent) alert('Please enter a note path');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch(`${this.config.apiEndpoint}/note`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify({ path, content })
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
|
|
301
|
+
if (data.success) {
|
|
302
|
+
this.state.currentNote = data.path;
|
|
303
|
+
this.elements.notePath.value = data.path;
|
|
304
|
+
if (!silent) this.showNotification('Note saved!', 'success');
|
|
305
|
+
this.loadStructure();
|
|
306
|
+
|
|
307
|
+
if (this.config.onSave) {
|
|
308
|
+
this.config.onSave(data.path, content);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.handleError('Failed to save note', error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async deleteNote() {
|
|
317
|
+
if (!this.state.currentNote) {
|
|
318
|
+
alert('No note to delete');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!confirm(`Delete "${this.state.currentNote}"?`)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const response = await fetch(`${this.config.apiEndpoint}/note`, {
|
|
328
|
+
method: 'DELETE',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({ path: this.state.currentNote })
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const data = await response.json();
|
|
334
|
+
|
|
335
|
+
if (data.success) {
|
|
336
|
+
this.createNewNote();
|
|
337
|
+
this.loadStructure();
|
|
338
|
+
this.showNotification('Note deleted!', 'success');
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
this.handleError('Failed to delete note', error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async uploadImageFile(file) {
|
|
346
|
+
try {
|
|
347
|
+
const formData = new FormData();
|
|
348
|
+
formData.append('image', file);
|
|
349
|
+
|
|
350
|
+
const response = await fetch(`${this.config.apiEndpoint}/upload-image`, {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
body: formData
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const data = await response.json();
|
|
356
|
+
|
|
357
|
+
if (data.imageUrl) {
|
|
358
|
+
const markdown = `\n`;
|
|
359
|
+
this.insertAtCursor(markdown);
|
|
360
|
+
this.showNotification('Image uploaded!', 'success');
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this.handleError('Failed to upload image', error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async uploadVideoFile(file) {
|
|
368
|
+
try {
|
|
369
|
+
const formData = new FormData();
|
|
370
|
+
formData.append('video', file);
|
|
371
|
+
|
|
372
|
+
const response = await fetch(`${this.config.apiEndpoint}/upload-video`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
body: formData
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const data = await response.json();
|
|
378
|
+
|
|
379
|
+
if (data.videoUrl) {
|
|
380
|
+
const markdown = `\n`;
|
|
381
|
+
this.insertAtCursor(markdown);
|
|
382
|
+
this.showNotification('Video uploaded!', 'success');
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
this.handleError('Failed to upload video', error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* UI Methods
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
createNewNote() {
|
|
394
|
+
this.state.currentNote = null;
|
|
395
|
+
this.elements.notePath.value = '';
|
|
396
|
+
this.elements.editorTextarea.value = '';
|
|
397
|
+
this.elements.previewContent.innerHTML = '';
|
|
398
|
+
this.elements.editorTextarea.focus();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
createFolder() {
|
|
402
|
+
const path = prompt('Enter folder path:');
|
|
403
|
+
if (!path) return;
|
|
404
|
+
|
|
405
|
+
fetch(`${this.config.apiEndpoint}/folder`, {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: { 'Content-Type': 'application/json' },
|
|
408
|
+
body: JSON.stringify({ path })
|
|
409
|
+
})
|
|
410
|
+
.then(res => res.json())
|
|
411
|
+
.then(data => {
|
|
412
|
+
if (data.success) {
|
|
413
|
+
this.loadStructure();
|
|
414
|
+
this.showNotification('Folder created!', 'success');
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.catch(error => this.handleError('Failed to create folder', error));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
uploadImage() {
|
|
421
|
+
const input = document.createElement('input');
|
|
422
|
+
input.type = 'file';
|
|
423
|
+
input.accept = 'image/*';
|
|
424
|
+
input.onchange = (e) => {
|
|
425
|
+
const file = e.target.files[0];
|
|
426
|
+
if (file) this.uploadImageFile(file);
|
|
427
|
+
};
|
|
428
|
+
input.click();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
uploadVideo() {
|
|
432
|
+
const input = document.createElement('input');
|
|
433
|
+
input.type = 'file';
|
|
434
|
+
input.accept = 'video/*';
|
|
435
|
+
input.onchange = (e) => {
|
|
436
|
+
const file = e.target.files[0];
|
|
437
|
+
if (file) this.uploadVideoFile(file);
|
|
438
|
+
};
|
|
439
|
+
input.click();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
togglePreview() {
|
|
443
|
+
const modes = ['editor', 'split', 'preview'];
|
|
444
|
+
const currentIndex = modes.indexOf(this.state.previewMode);
|
|
445
|
+
const nextIndex = (currentIndex + 1) % modes.length;
|
|
446
|
+
this.state.previewMode = modes[nextIndex];
|
|
447
|
+
|
|
448
|
+
// Use 'flex' instead of 'block' to maintain flex container layout
|
|
449
|
+
this.elements.editor.style.display =
|
|
450
|
+
this.state.previewMode === 'preview' ? 'none' : 'flex';
|
|
451
|
+
this.elements.preview.style.display =
|
|
452
|
+
this.state.previewMode === 'editor' ? 'none' : 'flex';
|
|
453
|
+
|
|
454
|
+
if (this.state.previewMode === 'split') {
|
|
455
|
+
this.elements.editor.style.width = '50%';
|
|
456
|
+
this.elements.preview.style.width = '50%';
|
|
457
|
+
} else {
|
|
458
|
+
this.elements.editor.style.width = '100%';
|
|
459
|
+
this.elements.preview.style.width = '100%';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (this.state.previewMode !== 'editor') {
|
|
463
|
+
this.renderPreview();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
toggleTheme() {
|
|
468
|
+
this.state.isDarkMode = !this.state.isDarkMode;
|
|
469
|
+
const container = document.querySelector(this.config.container);
|
|
470
|
+
container.classList.toggle('notes-dark-mode');
|
|
471
|
+
|
|
472
|
+
// Save preference to localStorage
|
|
473
|
+
localStorage.setItem('notesDarkMode', this.state.isDarkMode ? 'enabled' : 'disabled');
|
|
474
|
+
|
|
475
|
+
// Update theme button icon
|
|
476
|
+
const themeBtn = container.querySelector('[data-action="theme"]');
|
|
477
|
+
if (themeBtn) {
|
|
478
|
+
themeBtn.textContent = this.state.isDarkMode ? '☀️' : '🌙';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Switch highlight.js theme
|
|
482
|
+
const lightTheme = document.getElementById('hljs-light-theme');
|
|
483
|
+
const darkTheme = document.getElementById('hljs-dark-theme');
|
|
484
|
+
if (lightTheme && darkTheme) {
|
|
485
|
+
lightTheme.disabled = this.state.isDarkMode;
|
|
486
|
+
darkTheme.disabled = !this.state.isDarkMode;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
renderFileTree() {
|
|
491
|
+
this.elements.fileTree.innerHTML = '';
|
|
492
|
+
|
|
493
|
+
if (!this.state.structure || !Array.isArray(this.state.structure)) {
|
|
494
|
+
this.elements.fileTree.innerHTML = '<div style="padding: 10px; color: #999;">No notes yet</div>';
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (this.state.structure.length === 0) {
|
|
499
|
+
this.elements.fileTree.innerHTML = '<div style="padding: 10px; color: #999;">No notes yet. Create one to get started!</div>';
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Recursive function to render tree items
|
|
504
|
+
const renderTreeItems = (items, container, level = 0) => {
|
|
505
|
+
items.forEach(item => {
|
|
506
|
+
const itemDiv = document.createElement('div');
|
|
507
|
+
itemDiv.style.paddingLeft = `${level * 10}px`;
|
|
508
|
+
|
|
509
|
+
if (item.type === 'folder') {
|
|
510
|
+
const folderDiv = document.createElement('div');
|
|
511
|
+
folderDiv.className = 'notes-tree-item notes-tree-folder';
|
|
512
|
+
folderDiv.innerHTML = `
|
|
513
|
+
<span class="notes-tree-toggle">▶</span>
|
|
514
|
+
<span class="notes-tree-icon">📁</span>
|
|
515
|
+
<span>${item.name}</span>
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
const childrenDiv = document.createElement('div');
|
|
519
|
+
childrenDiv.className = 'notes-tree-children';
|
|
520
|
+
childrenDiv.style.display = 'none';
|
|
521
|
+
|
|
522
|
+
folderDiv.addEventListener('click', (e) => {
|
|
523
|
+
e.stopPropagation();
|
|
524
|
+
const isOpen = childrenDiv.style.display === 'block';
|
|
525
|
+
childrenDiv.style.display = isOpen ? 'none' : 'block';
|
|
526
|
+
folderDiv.querySelector('.notes-tree-toggle').textContent = isOpen ? '▶' : '▼';
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
itemDiv.appendChild(folderDiv);
|
|
530
|
+
itemDiv.appendChild(childrenDiv);
|
|
531
|
+
|
|
532
|
+
if (item.children && item.children.length > 0) {
|
|
533
|
+
renderTreeItems(item.children, childrenDiv, level + 1);
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
itemDiv.className = 'notes-tree-item notes-tree-file';
|
|
537
|
+
itemDiv.innerHTML = `
|
|
538
|
+
<span class="notes-tree-toggle"></span>
|
|
539
|
+
<span class="notes-tree-icon">📄</span>
|
|
540
|
+
<span>${item.name}</span>
|
|
541
|
+
`;
|
|
542
|
+
|
|
543
|
+
itemDiv.addEventListener('click', () => {
|
|
544
|
+
this.loadNote(item.path);
|
|
545
|
+
|
|
546
|
+
// Highlight active item
|
|
547
|
+
container.closest('.notes-file-tree').querySelectorAll('.notes-tree-item').forEach(el => {
|
|
548
|
+
el.classList.remove('active');
|
|
549
|
+
});
|
|
550
|
+
itemDiv.classList.add('active');
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
container.appendChild(itemDiv);
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
renderTreeItems(this.state.structure, this.elements.fileTree);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
renderPreview() {
|
|
562
|
+
if (!window.marked) {
|
|
563
|
+
setTimeout(() => this.renderPreview(), 100);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const markdown = this.elements.editorTextarea.value;
|
|
568
|
+
const basePath = this.elements.notePath.value;
|
|
569
|
+
|
|
570
|
+
// Create a custom renderer for internal links and videos
|
|
571
|
+
const renderer = {
|
|
572
|
+
link: (token) => {
|
|
573
|
+
const href = token.href;
|
|
574
|
+
const title = token.title || '';
|
|
575
|
+
const text = token.text;
|
|
576
|
+
|
|
577
|
+
// Check if it's an internal markdown link
|
|
578
|
+
if (href && href.endsWith('.md') && !href.startsWith('http')) {
|
|
579
|
+
// Resolve relative path based on current note location
|
|
580
|
+
const parts = basePath.split('/');
|
|
581
|
+
parts.pop(); // Remove filename
|
|
582
|
+
const dir = parts.join('/');
|
|
583
|
+
const linkPath = dir ? `${dir}/${href}`.replace(/\/\.\//g, '/') : href.replace('./', '');
|
|
584
|
+
|
|
585
|
+
return `<a href="#" class="internal-link" data-path="${linkPath}" title="${title}">${text}</a>`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return false; // Use default renderer
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
image: (token) => {
|
|
592
|
+
const href = token.href;
|
|
593
|
+
const title = token.title || '';
|
|
594
|
+
|
|
595
|
+
// Check if it's a video file
|
|
596
|
+
if (href &&
|
|
597
|
+
(href.endsWith('.mp4') || href.endsWith('.webm') ||
|
|
598
|
+
href.endsWith('.mov') || href.endsWith('.avi') ||
|
|
599
|
+
href.endsWith('.mkv') || href.includes('/videos/'))) {
|
|
600
|
+
return `<video controls style="max-width: 100%; height: auto;" title="${title}">
|
|
601
|
+
<source src="${href}" type="video/mp4">
|
|
602
|
+
Your browser does not support the video tag.
|
|
603
|
+
</video>`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return false; // Use default renderer
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Use marked with custom renderer
|
|
611
|
+
marked.use({ renderer });
|
|
612
|
+
const html = marked.parse(markdown);
|
|
613
|
+
this.elements.previewContent.innerHTML = html;
|
|
614
|
+
|
|
615
|
+
// Highlight code blocks
|
|
616
|
+
if (window.hljs) {
|
|
617
|
+
this.elements.previewContent.querySelectorAll('pre code').forEach(block => {
|
|
618
|
+
hljs.highlightBlock(block);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Add click handlers for internal links
|
|
623
|
+
this.elements.previewContent.querySelectorAll('.internal-link').forEach(link => {
|
|
624
|
+
link.addEventListener('click', (e) => {
|
|
625
|
+
e.preventDefault();
|
|
626
|
+
const linkPath = link.getAttribute('data-path');
|
|
627
|
+
if (linkPath) {
|
|
628
|
+
this.loadNote(linkPath);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
insertAtCursor(text) {
|
|
635
|
+
const textarea = this.elements.editorTextarea;
|
|
636
|
+
const start = textarea.selectionStart;
|
|
637
|
+
const end = textarea.selectionEnd;
|
|
638
|
+
const value = textarea.value;
|
|
639
|
+
|
|
640
|
+
textarea.value = value.substring(0, start) + text + value.substring(end);
|
|
641
|
+
textarea.selectionStart = textarea.selectionEnd = start + text.length;
|
|
642
|
+
textarea.focus();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
showNotification(message, type = 'info') {
|
|
646
|
+
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
647
|
+
// You can implement a custom notification system here
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
handleError(message, error) {
|
|
651
|
+
console.error(message, error);
|
|
652
|
+
if (this.config.onError) {
|
|
653
|
+
this.config.onError(message, error);
|
|
654
|
+
} else {
|
|
655
|
+
alert(message);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Export for different module systems
|
|
661
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
662
|
+
module.exports = { NotesEditor };
|
|
663
|
+
} else {
|
|
664
|
+
window.NotesEditor = NotesEditor;
|
|
665
|
+
}
|