wasibase 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 +69 -0
- package/bin/wasibase.js +5 -0
- package/package.json +54 -0
- package/src/config.js +11 -0
- package/src/index.js +54 -0
- package/src/storage.js +262 -0
- package/src/ui/backup.js +248 -0
- package/src/ui/graph.js +21 -0
- package/src/ui/manage.js +320 -0
- package/src/ui/note.js +449 -0
- package/src/ui/search.js +21 -0
- package/src/ui/sync.js +348 -0
- package/src/utils.js +104 -0
- package/src/web/graphServer.js +897 -0
- package/src/web/server.js +2132 -0
|
@@ -0,0 +1,2132 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { marked } from 'marked';
|
|
3
|
+
import * as storage from '../storage.js';
|
|
4
|
+
|
|
5
|
+
// Global state to track last known content for emergency saves
|
|
6
|
+
let lastKnownState = null;
|
|
7
|
+
|
|
8
|
+
// Setup graceful shutdown handlers
|
|
9
|
+
function setupGracefulShutdown(serverInstance, callback) {
|
|
10
|
+
const cleanup = async (signal) => {
|
|
11
|
+
console.log(`\n Received ${signal}, saving and shutting down...`);
|
|
12
|
+
|
|
13
|
+
// Save last known state if available
|
|
14
|
+
if (lastKnownState && lastKnownState.thema && lastKnownState.content) {
|
|
15
|
+
try {
|
|
16
|
+
const datum = new Date().toLocaleDateString('de-DE');
|
|
17
|
+
const fullContent = `---
|
|
18
|
+
Oberkategorie: ${lastKnownState.oberkategorie}
|
|
19
|
+
Unterkategorie: ${lastKnownState.unterkategorie}
|
|
20
|
+
Thema: ${lastKnownState.thema}
|
|
21
|
+
Erstellt: ${datum}
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
${lastKnownState.content}`;
|
|
25
|
+
|
|
26
|
+
storage.createOberkategorie(lastKnownState.oberkategorie);
|
|
27
|
+
storage.createUnterkategorie(lastKnownState.oberkategorie, lastKnownState.unterkategorie);
|
|
28
|
+
storage.saveNote(lastKnownState.oberkategorie, lastKnownState.unterkategorie, lastKnownState.thema, fullContent);
|
|
29
|
+
console.log(` Auto-saved: ${lastKnownState.thema}`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(' Failed to save on exit:', e.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (serverInstance) {
|
|
36
|
+
serverInstance.close();
|
|
37
|
+
}
|
|
38
|
+
if (callback) {
|
|
39
|
+
callback({ saved: !!lastKnownState?.thema, thema: lastKnownState?.thema || '' });
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Handle various termination signals
|
|
45
|
+
process.on('SIGINT', () => cleanup('SIGINT'));
|
|
46
|
+
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
|
47
|
+
process.on('SIGHUP', () => cleanup('SIGHUP'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function startServer(config, callback) {
|
|
51
|
+
const app = express();
|
|
52
|
+
app.use(express.json());
|
|
53
|
+
|
|
54
|
+
let serverInstance = null;
|
|
55
|
+
const { oberkategorie, unterkategorie, port = 3333 } = config;
|
|
56
|
+
|
|
57
|
+
// Reset last known state for this session
|
|
58
|
+
lastKnownState = { oberkategorie, unterkategorie, thema: '', content: '' };
|
|
59
|
+
|
|
60
|
+
app.get('/', (req, res) => {
|
|
61
|
+
res.send(getEditorHTML(oberkategorie, unterkategorie));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.post('/preview', (req, res) => {
|
|
65
|
+
const { markdown } = req.body;
|
|
66
|
+
const html = marked(markdown || '');
|
|
67
|
+
res.json({ html });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.post('/save', (req, res) => {
|
|
71
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
72
|
+
|
|
73
|
+
if (!thema || !thema.trim()) {
|
|
74
|
+
return res.status(400).json({ error: 'Thema fehlt' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const datum = new Date().toLocaleDateString('de-DE');
|
|
78
|
+
const fullContent = `---
|
|
79
|
+
Oberkategorie: ${oberkategorie}
|
|
80
|
+
Unterkategorie: ${unterkategorie}
|
|
81
|
+
Thema: ${thema.trim()}
|
|
82
|
+
Erstellt: ${datum}
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
${content}`;
|
|
86
|
+
|
|
87
|
+
storage.createOberkategorie(oberkategorie);
|
|
88
|
+
storage.createUnterkategorie(oberkategorie, unterkategorie);
|
|
89
|
+
storage.saveNote(oberkategorie, unterkategorie, thema.trim(), fullContent);
|
|
90
|
+
|
|
91
|
+
res.json({ success: true, path: `${oberkategorie}/${unterkategorie}/${thema}` });
|
|
92
|
+
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (serverInstance) serverInstance.close();
|
|
95
|
+
if (callback) callback({ saved: true, thema: thema.trim() });
|
|
96
|
+
}, 500);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
app.post('/cancel', (req, res) => {
|
|
100
|
+
res.json({ success: true });
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
if (serverInstance) serverInstance.close();
|
|
103
|
+
if (callback) callback({ saved: false });
|
|
104
|
+
}, 200);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Auto-Save Endpunkt - also updates lastKnownState for emergency saves
|
|
108
|
+
app.post('/autosave', (req, res) => {
|
|
109
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
110
|
+
|
|
111
|
+
// Always update lastKnownState for emergency saves on process termination
|
|
112
|
+
lastKnownState = { oberkategorie, unterkategorie, thema: thema?.trim() || '', content: content || '' };
|
|
113
|
+
|
|
114
|
+
if (!thema || !thema.trim()) {
|
|
115
|
+
return res.json({ success: false, reason: 'no_thema' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const datum = new Date().toLocaleDateString('de-DE');
|
|
119
|
+
const fullContent = `---
|
|
120
|
+
Oberkategorie: ${oberkategorie}
|
|
121
|
+
Unterkategorie: ${unterkategorie}
|
|
122
|
+
Thema: ${thema.trim()}
|
|
123
|
+
Erstellt: ${datum}
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
${content}`;
|
|
127
|
+
|
|
128
|
+
storage.createOberkategorie(oberkategorie);
|
|
129
|
+
storage.createUnterkategorie(oberkategorie, unterkategorie);
|
|
130
|
+
storage.saveNote(oberkategorie, unterkategorie, thema.trim(), fullContent);
|
|
131
|
+
|
|
132
|
+
res.json({ success: true, savedAt: new Date().toLocaleTimeString('de-DE') });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Heartbeat endpoint to keep track of current state
|
|
136
|
+
app.post('/heartbeat', (req, res) => {
|
|
137
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
138
|
+
lastKnownState = { oberkategorie, unterkategorie, thema: thema?.trim() || '', content: content || '' };
|
|
139
|
+
res.json({ success: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
serverInstance = app.listen(port);
|
|
143
|
+
|
|
144
|
+
// Setup graceful shutdown handlers
|
|
145
|
+
setupGracefulShutdown(serverInstance, callback);
|
|
146
|
+
|
|
147
|
+
return { port, close: () => serverInstance.close() };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function startEditServer(config, callback) {
|
|
151
|
+
const app = express();
|
|
152
|
+
app.use(express.json());
|
|
153
|
+
|
|
154
|
+
let serverInstance = null;
|
|
155
|
+
const { oberkategorie, unterkategorie, thema, content, port = 3333 } = config;
|
|
156
|
+
|
|
157
|
+
// Extrahiere nur den Inhalt ohne YAML Frontmatter
|
|
158
|
+
const contentWithoutFrontmatter = content ? content.replace(/^---[\s\S]*?---\n?/, '').trim() : '';
|
|
159
|
+
|
|
160
|
+
// Initialize last known state with existing content
|
|
161
|
+
lastKnownState = { oberkategorie, unterkategorie, thema, content: contentWithoutFrontmatter };
|
|
162
|
+
|
|
163
|
+
app.get('/', (req, res) => {
|
|
164
|
+
res.send(getEditHTML(oberkategorie, unterkategorie, thema, contentWithoutFrontmatter));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.post('/preview', (req, res) => {
|
|
168
|
+
const { markdown } = req.body;
|
|
169
|
+
const html = marked(markdown || '');
|
|
170
|
+
res.json({ html });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
app.post('/save', (req, res) => {
|
|
174
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
175
|
+
|
|
176
|
+
if (!thema || !thema.trim()) {
|
|
177
|
+
return res.status(400).json({ error: 'Thema fehlt' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const datum = new Date().toLocaleDateString('de-DE');
|
|
181
|
+
const fullContent = `---
|
|
182
|
+
Oberkategorie: ${oberkategorie}
|
|
183
|
+
Unterkategorie: ${unterkategorie}
|
|
184
|
+
Thema: ${thema.trim()}
|
|
185
|
+
Erstellt: ${datum}
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
${content}`;
|
|
189
|
+
|
|
190
|
+
storage.saveNote(oberkategorie, unterkategorie, thema.trim(), fullContent);
|
|
191
|
+
|
|
192
|
+
res.json({ success: true, path: `${oberkategorie}/${unterkategorie}/${thema}` });
|
|
193
|
+
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
if (serverInstance) serverInstance.close();
|
|
196
|
+
if (callback) callback({ saved: true, thema: thema.trim() });
|
|
197
|
+
}, 500);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
app.post('/cancel', (req, res) => {
|
|
201
|
+
res.json({ success: true });
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
if (serverInstance) serverInstance.close();
|
|
204
|
+
if (callback) callback({ saved: false });
|
|
205
|
+
}, 200);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
app.post('/autosave', (req, res) => {
|
|
209
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
210
|
+
|
|
211
|
+
// Always update lastKnownState for emergency saves
|
|
212
|
+
lastKnownState = { oberkategorie, unterkategorie, thema: thema?.trim() || '', content: content || '' };
|
|
213
|
+
|
|
214
|
+
if (!thema || !thema.trim()) {
|
|
215
|
+
return res.json({ success: false, reason: 'no_thema' });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const datum = new Date().toLocaleDateString('de-DE');
|
|
219
|
+
const fullContent = `---
|
|
220
|
+
Oberkategorie: ${oberkategorie}
|
|
221
|
+
Unterkategorie: ${unterkategorie}
|
|
222
|
+
Thema: ${thema.trim()}
|
|
223
|
+
Erstellt: ${datum}
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
${content}`;
|
|
227
|
+
|
|
228
|
+
storage.saveNote(oberkategorie, unterkategorie, thema.trim(), fullContent);
|
|
229
|
+
|
|
230
|
+
res.json({ success: true, savedAt: new Date().toLocaleTimeString('de-DE') });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Heartbeat endpoint to keep track of current state
|
|
234
|
+
app.post('/heartbeat', (req, res) => {
|
|
235
|
+
const { oberkategorie, unterkategorie, thema, content } = req.body;
|
|
236
|
+
lastKnownState = { oberkategorie, unterkategorie, thema: thema?.trim() || '', content: content || '' };
|
|
237
|
+
res.json({ success: true });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
serverInstance = app.listen(port);
|
|
241
|
+
|
|
242
|
+
// Setup graceful shutdown handlers
|
|
243
|
+
setupGracefulShutdown(serverInstance, callback);
|
|
244
|
+
|
|
245
|
+
return { port, close: () => serverInstance.close() };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function startSearchServer(config, callback) {
|
|
249
|
+
const app = express();
|
|
250
|
+
app.use(express.json());
|
|
251
|
+
|
|
252
|
+
let serverInstance = null;
|
|
253
|
+
const { port = 3334 } = config;
|
|
254
|
+
|
|
255
|
+
app.get('/', (req, res) => {
|
|
256
|
+
res.send(getSearchHTML());
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.post('/search', (req, res) => {
|
|
260
|
+
const { query } = req.body;
|
|
261
|
+
if (!query || !query.trim()) {
|
|
262
|
+
return res.json({ results: [] });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const results = storage.searchNotes(query.trim());
|
|
266
|
+
res.json({ results });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.post('/close', (req, res) => {
|
|
270
|
+
res.json({ success: true });
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
if (serverInstance) serverInstance.close();
|
|
273
|
+
if (callback) callback();
|
|
274
|
+
}, 200);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
serverInstance = app.listen(port);
|
|
278
|
+
return { port, close: () => serverInstance.close() };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getEditorHTML(oberkategorie, unterkategorie) {
|
|
282
|
+
return `<!DOCTYPE html>
|
|
283
|
+
<html lang="de">
|
|
284
|
+
<head>
|
|
285
|
+
<meta charset="UTF-8">
|
|
286
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
287
|
+
<title>Wasibase</title>
|
|
288
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
|
289
|
+
<style>
|
|
290
|
+
:root {
|
|
291
|
+
--bg-primary: #0a0a0a;
|
|
292
|
+
--bg-secondary: #141414;
|
|
293
|
+
--bg-tertiary: #1a1a1a;
|
|
294
|
+
--border: #262626;
|
|
295
|
+
--text-primary: #e5e5e5;
|
|
296
|
+
--text-secondary: #a3a3a3;
|
|
297
|
+
--text-muted: #737373;
|
|
298
|
+
--text-faint: #525252;
|
|
299
|
+
--code-bg: #1a1a1a;
|
|
300
|
+
--code-color: #f472b6;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
[data-theme="light"] {
|
|
304
|
+
--bg-primary: #ffffff;
|
|
305
|
+
--bg-secondary: #f5f5f5;
|
|
306
|
+
--bg-tertiary: #e5e5e5;
|
|
307
|
+
--border: #d4d4d4;
|
|
308
|
+
--text-primary: #171717;
|
|
309
|
+
--text-secondary: #404040;
|
|
310
|
+
--text-muted: #737373;
|
|
311
|
+
--text-faint: #a3a3a3;
|
|
312
|
+
--code-bg: #f5f5f5;
|
|
313
|
+
--code-color: #db2777;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
317
|
+
|
|
318
|
+
body {
|
|
319
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
320
|
+
background: var(--bg-primary);
|
|
321
|
+
color: var(--text-primary);
|
|
322
|
+
height: 100vh;
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
transition: background 0.2s, color 0.2s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
header {
|
|
329
|
+
background: var(--bg-secondary);
|
|
330
|
+
padding: 12px 20px;
|
|
331
|
+
border-bottom: 1px solid var(--border);
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 16px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.logo {
|
|
338
|
+
font-size: 18px;
|
|
339
|
+
font-weight: 700;
|
|
340
|
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
|
341
|
+
-webkit-background-clip: text;
|
|
342
|
+
-webkit-text-fill-color: transparent;
|
|
343
|
+
background-clip: text;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.breadcrumb {
|
|
347
|
+
color: var(--text-muted);
|
|
348
|
+
font-size: 13px;
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 8px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.breadcrumb span { color: var(--text-secondary); }
|
|
355
|
+
.breadcrumb .sep { color: var(--text-faint); }
|
|
356
|
+
|
|
357
|
+
.header-right {
|
|
358
|
+
margin-left: auto;
|
|
359
|
+
display: flex;
|
|
360
|
+
gap: 8px;
|
|
361
|
+
align-items: center;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.theme-toggle {
|
|
365
|
+
background: var(--bg-tertiary);
|
|
366
|
+
border: 1px solid var(--border);
|
|
367
|
+
color: var(--text-secondary);
|
|
368
|
+
padding: 6px 10px;
|
|
369
|
+
border-radius: 6px;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
font-size: 16px;
|
|
372
|
+
transition: all 0.15s;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.theme-toggle:hover {
|
|
376
|
+
background: var(--border);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
button {
|
|
380
|
+
padding: 8px 14px;
|
|
381
|
+
border-radius: 8px;
|
|
382
|
+
border: none;
|
|
383
|
+
font-size: 13px;
|
|
384
|
+
font-weight: 500;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
transition: all 0.15s;
|
|
387
|
+
display: flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
gap: 6px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.btn-secondary {
|
|
393
|
+
background: var(--bg-tertiary);
|
|
394
|
+
color: var(--text-secondary);
|
|
395
|
+
border: 1px solid var(--border);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.btn-secondary:hover { background: var(--border); color: var(--text-primary); }
|
|
399
|
+
|
|
400
|
+
.btn-primary {
|
|
401
|
+
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
402
|
+
color: white;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
406
|
+
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
407
|
+
|
|
408
|
+
.btn-done {
|
|
409
|
+
background: linear-gradient(135deg, #22c55e, #16a34a);
|
|
410
|
+
color: white;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.btn-done:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
414
|
+
|
|
415
|
+
.meta-bar {
|
|
416
|
+
background: var(--bg-secondary);
|
|
417
|
+
padding: 10px 20px;
|
|
418
|
+
border-bottom: 1px solid var(--border);
|
|
419
|
+
display: flex;
|
|
420
|
+
gap: 20px;
|
|
421
|
+
align-items: center;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.meta-field {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 8px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.meta-field label {
|
|
431
|
+
color: var(--text-muted);
|
|
432
|
+
font-size: 12px;
|
|
433
|
+
text-transform: uppercase;
|
|
434
|
+
letter-spacing: 0.5px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.meta-field input {
|
|
438
|
+
background: var(--bg-primary);
|
|
439
|
+
border: 1px solid var(--border);
|
|
440
|
+
border-radius: 6px;
|
|
441
|
+
padding: 8px 12px;
|
|
442
|
+
color: var(--text-primary);
|
|
443
|
+
font-size: 14px;
|
|
444
|
+
transition: all 0.15s;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.meta-field input:focus {
|
|
448
|
+
outline: none;
|
|
449
|
+
border-color: #3b82f6;
|
|
450
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.meta-field input[readonly] {
|
|
454
|
+
color: var(--text-muted);
|
|
455
|
+
background: var(--bg-secondary);
|
|
456
|
+
border-color: transparent;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#thema { width: 280px; }
|
|
460
|
+
|
|
461
|
+
.toolbar {
|
|
462
|
+
background: var(--bg-secondary);
|
|
463
|
+
padding: 8px 20px;
|
|
464
|
+
border-bottom: 1px solid var(--border);
|
|
465
|
+
display: flex;
|
|
466
|
+
gap: 4px;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.toolbar button {
|
|
470
|
+
padding: 6px 10px;
|
|
471
|
+
background: transparent;
|
|
472
|
+
color: var(--text-muted);
|
|
473
|
+
border-radius: 6px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.toolbar button:hover {
|
|
477
|
+
background: var(--border);
|
|
478
|
+
color: var(--text-primary);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.toolbar .sep {
|
|
482
|
+
width: 1px;
|
|
483
|
+
background: var(--border);
|
|
484
|
+
margin: 0 8px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
main {
|
|
488
|
+
flex: 1;
|
|
489
|
+
display: flex;
|
|
490
|
+
overflow: hidden;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.editor-pane, .preview-pane {
|
|
494
|
+
flex: 1;
|
|
495
|
+
display: flex;
|
|
496
|
+
flex-direction: column;
|
|
497
|
+
overflow: hidden;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.pane-header {
|
|
501
|
+
padding: 10px 16px;
|
|
502
|
+
background: var(--bg-secondary);
|
|
503
|
+
border-bottom: 1px solid var(--border);
|
|
504
|
+
font-size: 11px;
|
|
505
|
+
color: var(--text-faint);
|
|
506
|
+
text-transform: uppercase;
|
|
507
|
+
letter-spacing: 1px;
|
|
508
|
+
font-weight: 600;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.editor-pane { border-right: 1px solid var(--border); }
|
|
512
|
+
|
|
513
|
+
#editor {
|
|
514
|
+
flex: 1;
|
|
515
|
+
width: 100%;
|
|
516
|
+
background: var(--bg-primary);
|
|
517
|
+
color: var(--text-primary);
|
|
518
|
+
border: none;
|
|
519
|
+
padding: 20px;
|
|
520
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
521
|
+
font-size: 14px;
|
|
522
|
+
line-height: 1.7;
|
|
523
|
+
resize: none;
|
|
524
|
+
tab-size: 2;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#editor:focus { outline: none; }
|
|
528
|
+
#editor::placeholder { color: var(--text-faint); }
|
|
529
|
+
|
|
530
|
+
#preview {
|
|
531
|
+
flex: 1;
|
|
532
|
+
padding: 20px;
|
|
533
|
+
overflow-y: auto;
|
|
534
|
+
line-height: 1.7;
|
|
535
|
+
background: var(--bg-primary);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#preview h1 { font-size: 2em; margin-bottom: 16px; color: var(--text-primary); font-weight: 700; }
|
|
539
|
+
#preview h2 { font-size: 1.5em; margin: 32px 0 12px; color: var(--text-primary); font-weight: 600; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
540
|
+
#preview h3 { font-size: 1.25em; margin: 24px 0 8px; color: var(--text-primary); font-weight: 600; }
|
|
541
|
+
#preview p { margin-bottom: 16px; color: var(--text-secondary); }
|
|
542
|
+
#preview strong { color: var(--text-primary); font-weight: 600; }
|
|
543
|
+
#preview em { color: var(--text-secondary); }
|
|
544
|
+
#preview ul, #preview ol { margin: 16px 0; padding-left: 24px; color: var(--text-secondary); }
|
|
545
|
+
#preview li { margin: 8px 0; }
|
|
546
|
+
#preview code { background: var(--code-bg); padding: 3px 8px; border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--code-color); }
|
|
547
|
+
#preview pre { background: var(--bg-secondary); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; border: 1px solid var(--border); }
|
|
548
|
+
#preview pre code { background: none; padding: 0; color: var(--text-primary); }
|
|
549
|
+
#preview blockquote { border-left: 3px solid #3b82f6; padding-left: 16px; color: var(--text-muted); margin: 16px 0; font-style: italic; }
|
|
550
|
+
#preview a { color: #3b82f6; text-decoration: none; }
|
|
551
|
+
#preview a:hover { text-decoration: underline; }
|
|
552
|
+
#preview hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
|
|
553
|
+
#preview img { max-width: 100%; border-radius: 8px; }
|
|
554
|
+
|
|
555
|
+
.toast {
|
|
556
|
+
position: fixed;
|
|
557
|
+
bottom: 24px;
|
|
558
|
+
right: 24px;
|
|
559
|
+
background: var(--bg-secondary);
|
|
560
|
+
border: 1px solid var(--border);
|
|
561
|
+
color: var(--text-primary);
|
|
562
|
+
padding: 14px 20px;
|
|
563
|
+
border-radius: 10px;
|
|
564
|
+
font-size: 14px;
|
|
565
|
+
opacity: 0;
|
|
566
|
+
transform: translateY(10px);
|
|
567
|
+
transition: all 0.2s;
|
|
568
|
+
display: flex;
|
|
569
|
+
align-items: center;
|
|
570
|
+
gap: 10px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.toast.show { opacity: 1; transform: translateY(0); }
|
|
574
|
+
.toast.success { border-color: #22c55e; }
|
|
575
|
+
.toast.success::before { content: ''; display: block; width: 8px; height: 8px; background: #22c55e; border-radius: 50%; }
|
|
576
|
+
.toast.error { border-color: #ef4444; }
|
|
577
|
+
.toast.error::before { content: ''; display: block; width: 8px; height: 8px; background: #ef4444; border-radius: 50%; }
|
|
578
|
+
|
|
579
|
+
.shortcut {
|
|
580
|
+
font-size: 11px;
|
|
581
|
+
color: var(--text-faint);
|
|
582
|
+
background: var(--bg-tertiary);
|
|
583
|
+
padding: 2px 6px;
|
|
584
|
+
border-radius: 4px;
|
|
585
|
+
margin-left: 4px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.symbol-dropdown {
|
|
589
|
+
position: relative;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.symbol-panel {
|
|
593
|
+
display: none;
|
|
594
|
+
position: absolute;
|
|
595
|
+
top: 100%;
|
|
596
|
+
left: 0;
|
|
597
|
+
background: var(--bg-secondary);
|
|
598
|
+
border: 1px solid var(--border);
|
|
599
|
+
border-radius: 12px;
|
|
600
|
+
padding: 16px;
|
|
601
|
+
z-index: 100;
|
|
602
|
+
width: 320px;
|
|
603
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
604
|
+
max-height: 400px;
|
|
605
|
+
overflow-y: auto;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.symbol-panel.show { display: block; }
|
|
609
|
+
|
|
610
|
+
.symbol-category {
|
|
611
|
+
font-size: 11px;
|
|
612
|
+
color: var(--text-faint);
|
|
613
|
+
text-transform: uppercase;
|
|
614
|
+
letter-spacing: 1px;
|
|
615
|
+
margin: 12px 0 8px;
|
|
616
|
+
font-weight: 600;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.symbol-category:first-child { margin-top: 0; }
|
|
620
|
+
|
|
621
|
+
.symbol-grid {
|
|
622
|
+
display: grid;
|
|
623
|
+
grid-template-columns: repeat(8, 1fr);
|
|
624
|
+
gap: 4px;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.symbol-grid button {
|
|
628
|
+
padding: 8px;
|
|
629
|
+
font-size: 16px;
|
|
630
|
+
background: var(--bg-primary);
|
|
631
|
+
border: 1px solid var(--border);
|
|
632
|
+
border-radius: 6px;
|
|
633
|
+
color: var(--text-primary);
|
|
634
|
+
cursor: pointer;
|
|
635
|
+
transition: all 0.1s;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.symbol-grid button:hover {
|
|
639
|
+
background: #3b82f6;
|
|
640
|
+
border-color: #3b82f6;
|
|
641
|
+
color: white;
|
|
642
|
+
transform: scale(1.1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.save-status {
|
|
646
|
+
font-size: 12px;
|
|
647
|
+
padding: 4px 10px;
|
|
648
|
+
border-radius: 6px;
|
|
649
|
+
margin-left: auto;
|
|
650
|
+
margin-right: 8px;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.save-status.saved {
|
|
654
|
+
color: #22c55e;
|
|
655
|
+
background: rgba(34, 197, 94, 0.1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.save-status.unsaved {
|
|
659
|
+
color: #f59e0b;
|
|
660
|
+
background: rgba(245, 158, 11, 0.1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.save-status.saving {
|
|
664
|
+
color: #3b82f6;
|
|
665
|
+
background: rgba(59, 130, 246, 0.1);
|
|
666
|
+
}
|
|
667
|
+
</style>
|
|
668
|
+
</head>
|
|
669
|
+
<body>
|
|
670
|
+
<header>
|
|
671
|
+
<div class="logo">Wasibase</div>
|
|
672
|
+
<div class="breadcrumb">
|
|
673
|
+
<span>${oberkategorie}</span>
|
|
674
|
+
<span class="sep">/</span>
|
|
675
|
+
<span>${unterkategorie}</span>
|
|
676
|
+
</div>
|
|
677
|
+
<div class="header-right">
|
|
678
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Theme wechseln" id="themeBtn">🌙</button>
|
|
679
|
+
<span class="save-status" id="saveStatus"></span>
|
|
680
|
+
<button class="btn-secondary" onclick="cancel()">Abbrechen</button>
|
|
681
|
+
<button class="btn-primary" id="saveBtn" onclick="save()" disabled>Speichern <span class="shortcut">⌘S</span></button>
|
|
682
|
+
<button class="btn-done" id="doneBtn" onclick="done()">Beenden <span class="shortcut">Esc</span></button>
|
|
683
|
+
</div>
|
|
684
|
+
</header>
|
|
685
|
+
|
|
686
|
+
<div class="meta-bar">
|
|
687
|
+
<div class="meta-field">
|
|
688
|
+
<label>Thema</label>
|
|
689
|
+
<input type="text" id="thema" placeholder="Name der Note eingeben..." autofocus>
|
|
690
|
+
</div>
|
|
691
|
+
<input type="hidden" id="oberkategorie" value="${oberkategorie}">
|
|
692
|
+
<input type="hidden" id="unterkategorie" value="${unterkategorie}">
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<div class="toolbar">
|
|
696
|
+
<button onclick="insertFormat('**', '**')" title="Fett (Cmd+B)"><b>B</b></button>
|
|
697
|
+
<button onclick="insertFormat('*', '*')" title="Kursiv (Cmd+I)"><i>I</i></button>
|
|
698
|
+
<button onclick="insertFormat('~~', '~~')" title="Durchgestrichen"><s>S</s></button>
|
|
699
|
+
<div class="sep"></div>
|
|
700
|
+
<button onclick="insertLine('# ')" title="Ueberschrift 1">H1</button>
|
|
701
|
+
<button onclick="insertLine('## ')" title="Ueberschrift 2">H2</button>
|
|
702
|
+
<button onclick="insertLine('### ')" title="Ueberschrift 3">H3</button>
|
|
703
|
+
<div class="sep"></div>
|
|
704
|
+
<button onclick="insertLine('- ')" title="Liste">• Liste</button>
|
|
705
|
+
<button onclick="insertLine('1. ')" title="Nummerierte Liste">1. Liste</button>
|
|
706
|
+
<button onclick="insertLine('> ')" title="Zitat">" Zitat</button>
|
|
707
|
+
<div class="sep"></div>
|
|
708
|
+
<button onclick="insertFormat('\`', '\`')" title="Code"></></button>
|
|
709
|
+
<button onclick="insertCodeBlock()" title="Code-Block">Code</button>
|
|
710
|
+
<button onclick="insertFormat('[', '](url)')" title="Link">Link</button>
|
|
711
|
+
<div class="sep"></div>
|
|
712
|
+
<button onclick="insertFormat('[[', ']]')" title="Backlink">[[Link]]</button>
|
|
713
|
+
<div class="sep"></div>
|
|
714
|
+
<div class="symbol-dropdown">
|
|
715
|
+
<button onclick="toggleSymbols()" title="Mathematische Symbole">∑ Math</button>
|
|
716
|
+
<div class="symbol-panel" id="symbolPanel">
|
|
717
|
+
<div class="symbol-category">Griechisch</div>
|
|
718
|
+
<div class="symbol-grid">
|
|
719
|
+
<button onclick="insertSymbol('α')">α</button>
|
|
720
|
+
<button onclick="insertSymbol('β')">β</button>
|
|
721
|
+
<button onclick="insertSymbol('γ')">γ</button>
|
|
722
|
+
<button onclick="insertSymbol('δ')">δ</button>
|
|
723
|
+
<button onclick="insertSymbol('ε')">ε</button>
|
|
724
|
+
<button onclick="insertSymbol('θ')">θ</button>
|
|
725
|
+
<button onclick="insertSymbol('λ')">λ</button>
|
|
726
|
+
<button onclick="insertSymbol('μ')">μ</button>
|
|
727
|
+
<button onclick="insertSymbol('π')">π</button>
|
|
728
|
+
<button onclick="insertSymbol('σ')">σ</button>
|
|
729
|
+
<button onclick="insertSymbol('φ')">φ</button>
|
|
730
|
+
<button onclick="insertSymbol('ω')">ω</button>
|
|
731
|
+
<button onclick="insertSymbol('Γ')">Γ</button>
|
|
732
|
+
<button onclick="insertSymbol('Δ')">Δ</button>
|
|
733
|
+
<button onclick="insertSymbol('Θ')">Θ</button>
|
|
734
|
+
<button onclick="insertSymbol('Λ')">Λ</button>
|
|
735
|
+
<button onclick="insertSymbol('Σ')">Σ</button>
|
|
736
|
+
<button onclick="insertSymbol('Φ')">Φ</button>
|
|
737
|
+
<button onclick="insertSymbol('Ω')">Ω</button>
|
|
738
|
+
</div>
|
|
739
|
+
<div class="symbol-category">Operatoren</div>
|
|
740
|
+
<div class="symbol-grid">
|
|
741
|
+
<button onclick="insertSymbol('±')">±</button>
|
|
742
|
+
<button onclick="insertSymbol('×')">×</button>
|
|
743
|
+
<button onclick="insertSymbol('÷')">÷</button>
|
|
744
|
+
<button onclick="insertSymbol('·')">·</button>
|
|
745
|
+
<button onclick="insertSymbol('∞')">∞</button>
|
|
746
|
+
<button onclick="insertSymbol('√')">√</button>
|
|
747
|
+
<button onclick="insertSymbol('∫')">∫</button>
|
|
748
|
+
<button onclick="insertSymbol('∂')">∂</button>
|
|
749
|
+
<button onclick="insertSymbol('∇')">∇</button>
|
|
750
|
+
<button onclick="insertSymbol('∑')">∑</button>
|
|
751
|
+
<button onclick="insertSymbol('∏')">∏</button>
|
|
752
|
+
</div>
|
|
753
|
+
<div class="symbol-category">Relationen</div>
|
|
754
|
+
<div class="symbol-grid">
|
|
755
|
+
<button onclick="insertSymbol('≠')">≠</button>
|
|
756
|
+
<button onclick="insertSymbol('≈')">≈</button>
|
|
757
|
+
<button onclick="insertSymbol('≤')">≤</button>
|
|
758
|
+
<button onclick="insertSymbol('≥')">≥</button>
|
|
759
|
+
<button onclick="insertSymbol('≡')">≡</button>
|
|
760
|
+
<button onclick="insertSymbol('∝')">∝</button>
|
|
761
|
+
<button onclick="insertSymbol('⊂')">⊂</button>
|
|
762
|
+
<button onclick="insertSymbol('⊃')">⊃</button>
|
|
763
|
+
<button onclick="insertSymbol('∈')">∈</button>
|
|
764
|
+
<button onclick="insertSymbol('∉')">∉</button>
|
|
765
|
+
<button onclick="insertSymbol('∪')">∪</button>
|
|
766
|
+
<button onclick="insertSymbol('∩')">∩</button>
|
|
767
|
+
</div>
|
|
768
|
+
<div class="symbol-category">Matrix / Vektor</div>
|
|
769
|
+
<div class="symbol-grid">
|
|
770
|
+
<button onclick="insertSymbol('ᵀ')" title="Transponiert">ᵀ</button>
|
|
771
|
+
<button onclick="insertSymbol('⁻¹')" title="Inverse">⁻¹</button>
|
|
772
|
+
<button onclick="insertSymbol('→')">→</button>
|
|
773
|
+
<button onclick="insertSymbol('⟨')">⟨</button>
|
|
774
|
+
<button onclick="insertSymbol('⟩')">⟩</button>
|
|
775
|
+
<button onclick="insertSymbol('‖')">‖</button>
|
|
776
|
+
<button onclick="insertSymbol('⊗')">⊗</button>
|
|
777
|
+
<button onclick="insertSymbol('⊕')">⊕</button>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="symbol-category">Lineare Algebra</div>
|
|
780
|
+
<div class="symbol-grid">
|
|
781
|
+
<button onclick="insertSymbol('ᵀ')" title="Transponiert">ᵀ</button>
|
|
782
|
+
<button onclick="insertSymbol('⁻¹')" title="Inverse">⁻¹</button>
|
|
783
|
+
<button onclick="insertSymbol('det')" title="Determinante">det</button>
|
|
784
|
+
<button onclick="insertSymbol('tr')" title="Spur">tr</button>
|
|
785
|
+
<button onclick="insertSymbol('rk')" title="Rang">rk</button>
|
|
786
|
+
<button onclick="insertSymbol('ker')" title="Kern">ker</button>
|
|
787
|
+
<button onclick="insertSymbol('im')" title="Bild">im</button>
|
|
788
|
+
<button onclick="insertSymbol('dim')" title="Dimension">dim</button>
|
|
789
|
+
<button onclick="insertSymbol('span')" title="Aufspann">span</button>
|
|
790
|
+
<button onclick="insertSymbol('⊥')" title="Orthogonal">⊥</button>
|
|
791
|
+
<button onclick="insertSymbol('‖')" title="Norm">‖</button>
|
|
792
|
+
<button onclick="insertSymbol('⟨')" title="Skalarprodukt"><</button>
|
|
793
|
+
<button onclick="insertSymbol('⟩')" title="Skalarprodukt">></button>
|
|
794
|
+
<button onclick="insertSymbol('⊗')" title="Tensorprodukt">⊗</button>
|
|
795
|
+
<button onclick="insertSymbol('⊕')" title="Direkte Summe">⊕</button>
|
|
796
|
+
<button onclick="insertSymbol('→')" title="Abbildung">→</button>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="symbol-category">Hoch-/Tiefgestellt</div>
|
|
799
|
+
<div class="symbol-grid">
|
|
800
|
+
<button onclick="insertSymbol('⁰')">⁰</button>
|
|
801
|
+
<button onclick="insertSymbol('¹')">¹</button>
|
|
802
|
+
<button onclick="insertSymbol('²')">²</button>
|
|
803
|
+
<button onclick="insertSymbol('³')">³</button>
|
|
804
|
+
<button onclick="insertSymbol('ⁿ')">ⁿ</button>
|
|
805
|
+
<button onclick="insertSymbol('₀')">₀</button>
|
|
806
|
+
<button onclick="insertSymbol('₁')">₁</button>
|
|
807
|
+
<button onclick="insertSymbol('₂')">₂</button>
|
|
808
|
+
<button onclick="insertSymbol('ᵢ')">ᵢ</button>
|
|
809
|
+
<button onclick="insertSymbol('ⱼ')">ⱼ</button>
|
|
810
|
+
<button onclick="insertSymbol('ₙ')">ₙ</button>
|
|
811
|
+
<button onclick="insertSymbol('ₘ')">ₘ</button>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
<main>
|
|
818
|
+
<div class="editor-pane">
|
|
819
|
+
<div class="pane-header">Editor</div>
|
|
820
|
+
<textarea id="editor" placeholder="Beginne zu schreiben...
|
|
821
|
+
|
|
822
|
+
Markdown-Syntax:
|
|
823
|
+
# Ueberschrift
|
|
824
|
+
**fett** und *kursiv*
|
|
825
|
+
- Aufzaehlung
|
|
826
|
+
> Zitat
|
|
827
|
+
\`code\`
|
|
828
|
+
|
|
829
|
+
Backlinks zu anderen Notes:
|
|
830
|
+
[[Anderes Thema]]"></textarea>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="preview-pane">
|
|
833
|
+
<div class="pane-header">Vorschau</div>
|
|
834
|
+
<div id="preview"></div>
|
|
835
|
+
</div>
|
|
836
|
+
</main>
|
|
837
|
+
|
|
838
|
+
<div class="toast" id="toast"></div>
|
|
839
|
+
|
|
840
|
+
<script>
|
|
841
|
+
const editor = document.getElementById('editor');
|
|
842
|
+
const preview = document.getElementById('preview');
|
|
843
|
+
const themaInput = document.getElementById('thema');
|
|
844
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
845
|
+
const toast = document.getElementById('toast');
|
|
846
|
+
|
|
847
|
+
let debounceTimer;
|
|
848
|
+
|
|
849
|
+
editor.addEventListener('input', () => {
|
|
850
|
+
clearTimeout(debounceTimer);
|
|
851
|
+
debounceTimer = setTimeout(updatePreview, 100);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
themaInput.addEventListener('input', () => {
|
|
855
|
+
saveBtn.disabled = !themaInput.value.trim();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
async function updatePreview() {
|
|
859
|
+
const res = await fetch('/preview', {
|
|
860
|
+
method: 'POST',
|
|
861
|
+
headers: { 'Content-Type': 'application/json' },
|
|
862
|
+
body: JSON.stringify({ markdown: editor.value })
|
|
863
|
+
});
|
|
864
|
+
const { html } = await res.json();
|
|
865
|
+
preview.innerHTML = DOMPurify.sanitize(html);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function insertFormat(before, after) {
|
|
869
|
+
const start = editor.selectionStart;
|
|
870
|
+
const end = editor.selectionEnd;
|
|
871
|
+
const text = editor.value;
|
|
872
|
+
const selected = text.substring(start, end) || 'text';
|
|
873
|
+
editor.value = text.substring(0, start) + before + selected + after + text.substring(end);
|
|
874
|
+
editor.focus();
|
|
875
|
+
editor.selectionStart = start + before.length;
|
|
876
|
+
editor.selectionEnd = start + before.length + selected.length;
|
|
877
|
+
updatePreview();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function insertLine(prefix) {
|
|
881
|
+
const start = editor.selectionStart;
|
|
882
|
+
const text = editor.value;
|
|
883
|
+
const lineStart = text.lastIndexOf('\\n', start - 1) + 1;
|
|
884
|
+
editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
|
885
|
+
editor.focus();
|
|
886
|
+
editor.selectionStart = editor.selectionEnd = lineStart + prefix.length;
|
|
887
|
+
updatePreview();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function insertCodeBlock() {
|
|
891
|
+
insertFormat('\\n\`\`\`\\n', '\\n\`\`\`\\n');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function toggleSymbols() {
|
|
895
|
+
document.getElementById('symbolPanel').classList.toggle('show');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function insertSymbol(symbol) {
|
|
899
|
+
const start = editor.selectionStart;
|
|
900
|
+
editor.value = editor.value.substring(0, start) + symbol + editor.value.substring(editor.selectionEnd);
|
|
901
|
+
editor.focus();
|
|
902
|
+
editor.selectionStart = editor.selectionEnd = start + symbol.length;
|
|
903
|
+
updatePreview();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Schliesse Symbol-Panel bei Klick ausserhalb
|
|
907
|
+
document.addEventListener('click', (e) => {
|
|
908
|
+
const panel = document.getElementById('symbolPanel');
|
|
909
|
+
const dropdown = e.target.closest('.symbol-dropdown');
|
|
910
|
+
if (!dropdown && panel.classList.contains('show')) {
|
|
911
|
+
panel.classList.remove('show');
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
async function save() {
|
|
916
|
+
const thema = themaInput.value.trim();
|
|
917
|
+
if (!thema) {
|
|
918
|
+
showToast('Bitte Thema eingeben', 'error');
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const res = await fetch('/autosave', {
|
|
923
|
+
method: 'POST',
|
|
924
|
+
headers: { 'Content-Type': 'application/json' },
|
|
925
|
+
body: JSON.stringify({
|
|
926
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
927
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
928
|
+
thema,
|
|
929
|
+
content: editor.value
|
|
930
|
+
})
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const data = await res.json();
|
|
934
|
+
if (data.success) {
|
|
935
|
+
lastSavedContent = editor.value;
|
|
936
|
+
lastSavedThema = thema;
|
|
937
|
+
hasUnsavedChanges = false;
|
|
938
|
+
showToast('Gespeichert!', 'success');
|
|
939
|
+
updateSaveStatus();
|
|
940
|
+
return true;
|
|
941
|
+
} else {
|
|
942
|
+
showToast(data.error || 'Fehler', 'error');
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function done() {
|
|
948
|
+
if (themaInput.value.trim() && editor.value) {
|
|
949
|
+
await save();
|
|
950
|
+
}
|
|
951
|
+
await fetch('/cancel', { method: 'POST' });
|
|
952
|
+
window.close();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function cancel() {
|
|
956
|
+
await fetch('/cancel', { method: 'POST' });
|
|
957
|
+
window.close();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function showToast(message, type = '') {
|
|
961
|
+
toast.textContent = message;
|
|
962
|
+
toast.className = 'toast show ' + type;
|
|
963
|
+
setTimeout(() => toast.className = 'toast', 3000);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
document.addEventListener('keydown', (e) => {
|
|
967
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
968
|
+
e.preventDefault();
|
|
969
|
+
if (!saveBtn.disabled) save();
|
|
970
|
+
}
|
|
971
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
972
|
+
e.preventDefault();
|
|
973
|
+
insertFormat('**', '**');
|
|
974
|
+
}
|
|
975
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'i') {
|
|
976
|
+
e.preventDefault();
|
|
977
|
+
insertFormat('*', '*');
|
|
978
|
+
}
|
|
979
|
+
if (e.key === 'Escape') done();
|
|
980
|
+
if (e.key === 'Tab' && document.activeElement === editor) {
|
|
981
|
+
e.preventDefault();
|
|
982
|
+
const start = editor.selectionStart;
|
|
983
|
+
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(editor.selectionEnd);
|
|
984
|
+
editor.selectionStart = editor.selectionEnd = start + 2;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Auto-Save
|
|
989
|
+
let lastSavedContent = '';
|
|
990
|
+
let lastSavedThema = '';
|
|
991
|
+
let autoSaveTimer;
|
|
992
|
+
let hasUnsavedChanges = false;
|
|
993
|
+
|
|
994
|
+
function checkForChanges() {
|
|
995
|
+
const currentContent = editor.value;
|
|
996
|
+
const currentThema = themaInput.value.trim();
|
|
997
|
+
hasUnsavedChanges = (currentContent !== lastSavedContent || currentThema !== lastSavedThema);
|
|
998
|
+
updateSaveStatus();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function updateSaveStatus() {
|
|
1002
|
+
const status = document.getElementById('saveStatus');
|
|
1003
|
+
if (!status) return;
|
|
1004
|
+
|
|
1005
|
+
if (hasUnsavedChanges && themaInput.value.trim()) {
|
|
1006
|
+
status.textContent = 'Ungespeichert';
|
|
1007
|
+
status.className = 'save-status unsaved';
|
|
1008
|
+
} else if (lastSavedContent) {
|
|
1009
|
+
status.textContent = 'Gespeichert';
|
|
1010
|
+
status.className = 'save-status saved';
|
|
1011
|
+
} else {
|
|
1012
|
+
status.textContent = '';
|
|
1013
|
+
status.className = 'save-status';
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function autoSave() {
|
|
1018
|
+
const thema = themaInput.value.trim();
|
|
1019
|
+
const content = editor.value;
|
|
1020
|
+
|
|
1021
|
+
if (!thema || !content) return;
|
|
1022
|
+
if (content === lastSavedContent && thema === lastSavedThema) return;
|
|
1023
|
+
|
|
1024
|
+
const status = document.getElementById('saveStatus');
|
|
1025
|
+
if (status) {
|
|
1026
|
+
status.textContent = 'Speichert...';
|
|
1027
|
+
status.className = 'save-status saving';
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const res = await fetch('/autosave', {
|
|
1032
|
+
method: 'POST',
|
|
1033
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1034
|
+
body: JSON.stringify({
|
|
1035
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1036
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1037
|
+
thema,
|
|
1038
|
+
content
|
|
1039
|
+
})
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const data = await res.json();
|
|
1043
|
+
if (data.success) {
|
|
1044
|
+
lastSavedContent = content;
|
|
1045
|
+
lastSavedThema = thema;
|
|
1046
|
+
hasUnsavedChanges = false;
|
|
1047
|
+
if (status) {
|
|
1048
|
+
status.textContent = 'Gespeichert ' + data.savedAt;
|
|
1049
|
+
status.className = 'save-status saved';
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
console.error('Auto-save failed:', e);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Trigger auto-save on changes
|
|
1058
|
+
editor.addEventListener('input', () => {
|
|
1059
|
+
checkForChanges();
|
|
1060
|
+
clearTimeout(autoSaveTimer);
|
|
1061
|
+
autoSaveTimer = setTimeout(autoSave, 3000);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
themaInput.addEventListener('input', () => {
|
|
1065
|
+
saveBtn.disabled = !themaInput.value.trim();
|
|
1066
|
+
checkForChanges();
|
|
1067
|
+
clearTimeout(autoSaveTimer);
|
|
1068
|
+
autoSaveTimer = setTimeout(autoSave, 3000);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// Warnung beim Schliessen - save immediately
|
|
1072
|
+
window.addEventListener('beforeunload', (e) => {
|
|
1073
|
+
if (hasUnsavedChanges && themaInput.value.trim()) {
|
|
1074
|
+
// Try to save synchronously before page unloads
|
|
1075
|
+
navigator.sendBeacon('/autosave', JSON.stringify({
|
|
1076
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1077
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1078
|
+
thema: themaInput.value.trim(),
|
|
1079
|
+
content: editor.value
|
|
1080
|
+
}));
|
|
1081
|
+
e.preventDefault();
|
|
1082
|
+
e.returnValue = '';
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Save when tab becomes hidden (user switches tabs/apps)
|
|
1087
|
+
document.addEventListener('visibilitychange', () => {
|
|
1088
|
+
if (document.hidden && themaInput.value.trim() && editor.value) {
|
|
1089
|
+
autoSave();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// Heartbeat - send current state to server every 5 seconds for emergency saves
|
|
1094
|
+
// Also checks if server is still alive - closes window if not
|
|
1095
|
+
let serverCheckFailures = 0;
|
|
1096
|
+
setInterval(async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
const res = await fetch('/heartbeat', {
|
|
1099
|
+
method: 'POST',
|
|
1100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1101
|
+
body: JSON.stringify({
|
|
1102
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1103
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1104
|
+
thema: themaInput.value.trim(),
|
|
1105
|
+
content: editor.value
|
|
1106
|
+
})
|
|
1107
|
+
});
|
|
1108
|
+
if (res.ok) {
|
|
1109
|
+
serverCheckFailures = 0;
|
|
1110
|
+
} else {
|
|
1111
|
+
serverCheckFailures++;
|
|
1112
|
+
}
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
serverCheckFailures++;
|
|
1115
|
+
if (serverCheckFailures >= 2) {
|
|
1116
|
+
// Server is gone, close the window
|
|
1117
|
+
window.close();
|
|
1118
|
+
// Fallback: show message if window.close() doesn't work
|
|
1119
|
+
document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;flex-direction:column;background:#0a0a0a;color:#666;font-family:system-ui;"><h2 style="color:#fff;margin-bottom:16px;">Server beendet</h2><p>Du kannst dieses Fenster schliessen.</p></div>';
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}, 3000);
|
|
1123
|
+
|
|
1124
|
+
// Theme Toggle
|
|
1125
|
+
function toggleTheme() {
|
|
1126
|
+
const html = document.documentElement;
|
|
1127
|
+
const btn = document.getElementById('themeBtn');
|
|
1128
|
+
const currentTheme = html.getAttribute('data-theme');
|
|
1129
|
+
|
|
1130
|
+
if (currentTheme === 'light') {
|
|
1131
|
+
html.removeAttribute('data-theme');
|
|
1132
|
+
btn.textContent = '🌙';
|
|
1133
|
+
localStorage.setItem('wasibase-theme', 'dark');
|
|
1134
|
+
} else {
|
|
1135
|
+
html.setAttribute('data-theme', 'light');
|
|
1136
|
+
btn.textContent = '☀️';
|
|
1137
|
+
localStorage.setItem('wasibase-theme', 'light');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Theme beim Laden wiederherstellen
|
|
1142
|
+
(function() {
|
|
1143
|
+
const savedTheme = localStorage.getItem('wasibase-theme');
|
|
1144
|
+
const btn = document.getElementById('themeBtn');
|
|
1145
|
+
if (savedTheme === 'light') {
|
|
1146
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1147
|
+
btn.textContent = '☀️';
|
|
1148
|
+
}
|
|
1149
|
+
})();
|
|
1150
|
+
|
|
1151
|
+
themaInput.focus();
|
|
1152
|
+
</script>
|
|
1153
|
+
</body>
|
|
1154
|
+
</html>`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function getEditHTML(oberkategorie, unterkategorie, thema, content) {
|
|
1158
|
+
const escapedContent = content.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
1159
|
+
const escapedThema = thema.replace(/'/g, "\\'").replace(/"/g, '"');
|
|
1160
|
+
|
|
1161
|
+
return `<!DOCTYPE html>
|
|
1162
|
+
<html lang="de">
|
|
1163
|
+
<head>
|
|
1164
|
+
<meta charset="UTF-8">
|
|
1165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1166
|
+
<title>Bearbeiten: ${thema} - Wasibase</title>
|
|
1167
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
|
1168
|
+
<style>
|
|
1169
|
+
:root {
|
|
1170
|
+
--bg-primary: #0a0a0a;
|
|
1171
|
+
--bg-secondary: #141414;
|
|
1172
|
+
--bg-tertiary: #1a1a1a;
|
|
1173
|
+
--border: #262626;
|
|
1174
|
+
--text-primary: #e5e5e5;
|
|
1175
|
+
--text-secondary: #a3a3a3;
|
|
1176
|
+
--text-muted: #737373;
|
|
1177
|
+
--text-faint: #525252;
|
|
1178
|
+
--code-bg: #1a1a1a;
|
|
1179
|
+
--code-color: #f472b6;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
[data-theme="light"] {
|
|
1183
|
+
--bg-primary: #ffffff;
|
|
1184
|
+
--bg-secondary: #f5f5f5;
|
|
1185
|
+
--bg-tertiary: #e5e5e5;
|
|
1186
|
+
--border: #d4d4d4;
|
|
1187
|
+
--text-primary: #171717;
|
|
1188
|
+
--text-secondary: #404040;
|
|
1189
|
+
--text-muted: #737373;
|
|
1190
|
+
--text-faint: #a3a3a3;
|
|
1191
|
+
--code-bg: #f5f5f5;
|
|
1192
|
+
--code-color: #db2777;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1196
|
+
|
|
1197
|
+
body {
|
|
1198
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
1199
|
+
background: var(--bg-primary);
|
|
1200
|
+
color: var(--text-primary);
|
|
1201
|
+
height: 100vh;
|
|
1202
|
+
display: flex;
|
|
1203
|
+
flex-direction: column;
|
|
1204
|
+
transition: background 0.2s, color 0.2s;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
header {
|
|
1208
|
+
background: var(--bg-secondary);
|
|
1209
|
+
padding: 12px 20px;
|
|
1210
|
+
border-bottom: 1px solid var(--border);
|
|
1211
|
+
display: flex;
|
|
1212
|
+
align-items: center;
|
|
1213
|
+
gap: 16px;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.logo {
|
|
1217
|
+
font-size: 18px;
|
|
1218
|
+
font-weight: 700;
|
|
1219
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
1220
|
+
-webkit-background-clip: text;
|
|
1221
|
+
-webkit-text-fill-color: transparent;
|
|
1222
|
+
background-clip: text;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.breadcrumb {
|
|
1226
|
+
color: var(--text-muted);
|
|
1227
|
+
font-size: 13px;
|
|
1228
|
+
display: flex;
|
|
1229
|
+
align-items: center;
|
|
1230
|
+
gap: 8px;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.breadcrumb span { color: var(--text-secondary); }
|
|
1234
|
+
.breadcrumb .sep { color: var(--text-faint); }
|
|
1235
|
+
|
|
1236
|
+
.header-right {
|
|
1237
|
+
margin-left: auto;
|
|
1238
|
+
display: flex;
|
|
1239
|
+
gap: 8px;
|
|
1240
|
+
align-items: center;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.theme-toggle {
|
|
1244
|
+
background: var(--bg-tertiary);
|
|
1245
|
+
border: 1px solid var(--border);
|
|
1246
|
+
color: var(--text-secondary);
|
|
1247
|
+
padding: 6px 10px;
|
|
1248
|
+
border-radius: 6px;
|
|
1249
|
+
cursor: pointer;
|
|
1250
|
+
font-size: 16px;
|
|
1251
|
+
transition: all 0.15s;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.theme-toggle:hover {
|
|
1255
|
+
background: var(--border);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
button {
|
|
1259
|
+
padding: 8px 14px;
|
|
1260
|
+
border-radius: 8px;
|
|
1261
|
+
border: none;
|
|
1262
|
+
font-size: 13px;
|
|
1263
|
+
font-weight: 500;
|
|
1264
|
+
cursor: pointer;
|
|
1265
|
+
transition: all 0.15s;
|
|
1266
|
+
display: flex;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
gap: 6px;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.btn-secondary {
|
|
1272
|
+
background: var(--bg-tertiary);
|
|
1273
|
+
color: var(--text-secondary);
|
|
1274
|
+
border: 1px solid var(--border);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.btn-secondary:hover { background: var(--border); color: var(--text-primary); }
|
|
1278
|
+
|
|
1279
|
+
.btn-primary {
|
|
1280
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
1281
|
+
color: white;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
1285
|
+
|
|
1286
|
+
.btn-done {
|
|
1287
|
+
background: linear-gradient(135deg, #22c55e, #16a34a);
|
|
1288
|
+
color: white;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.btn-done:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
1292
|
+
|
|
1293
|
+
.meta-bar {
|
|
1294
|
+
background: var(--bg-secondary);
|
|
1295
|
+
padding: 10px 20px;
|
|
1296
|
+
border-bottom: 1px solid var(--border);
|
|
1297
|
+
display: flex;
|
|
1298
|
+
gap: 20px;
|
|
1299
|
+
align-items: center;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.meta-field {
|
|
1303
|
+
display: flex;
|
|
1304
|
+
align-items: center;
|
|
1305
|
+
gap: 8px;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.meta-field label {
|
|
1309
|
+
color: var(--text-muted);
|
|
1310
|
+
font-size: 12px;
|
|
1311
|
+
text-transform: uppercase;
|
|
1312
|
+
letter-spacing: 0.5px;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.meta-field input {
|
|
1316
|
+
background: var(--bg-primary);
|
|
1317
|
+
border: 1px solid var(--border);
|
|
1318
|
+
border-radius: 6px;
|
|
1319
|
+
padding: 8px 12px;
|
|
1320
|
+
color: var(--text-primary);
|
|
1321
|
+
font-size: 14px;
|
|
1322
|
+
transition: all 0.15s;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.meta-field input:focus {
|
|
1326
|
+
outline: none;
|
|
1327
|
+
border-color: #f59e0b;
|
|
1328
|
+
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
#thema { width: 280px; }
|
|
1332
|
+
|
|
1333
|
+
.edit-badge {
|
|
1334
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
1335
|
+
color: white;
|
|
1336
|
+
font-size: 10px;
|
|
1337
|
+
font-weight: 600;
|
|
1338
|
+
padding: 4px 8px;
|
|
1339
|
+
border-radius: 4px;
|
|
1340
|
+
text-transform: uppercase;
|
|
1341
|
+
letter-spacing: 0.5px;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.toolbar {
|
|
1345
|
+
background: var(--bg-secondary);
|
|
1346
|
+
padding: 8px 20px;
|
|
1347
|
+
border-bottom: 1px solid var(--border);
|
|
1348
|
+
display: flex;
|
|
1349
|
+
gap: 4px;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.toolbar button {
|
|
1353
|
+
padding: 6px 10px;
|
|
1354
|
+
background: transparent;
|
|
1355
|
+
color: var(--text-muted);
|
|
1356
|
+
border-radius: 6px;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.toolbar button:hover {
|
|
1360
|
+
background: var(--border);
|
|
1361
|
+
color: var(--text-primary);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.toolbar .sep {
|
|
1365
|
+
width: 1px;
|
|
1366
|
+
background: var(--border);
|
|
1367
|
+
margin: 0 8px;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
main {
|
|
1371
|
+
flex: 1;
|
|
1372
|
+
display: flex;
|
|
1373
|
+
overflow: hidden;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.editor-pane, .preview-pane {
|
|
1377
|
+
flex: 1;
|
|
1378
|
+
display: flex;
|
|
1379
|
+
flex-direction: column;
|
|
1380
|
+
overflow: hidden;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.pane-header {
|
|
1384
|
+
padding: 10px 16px;
|
|
1385
|
+
background: var(--bg-secondary);
|
|
1386
|
+
border-bottom: 1px solid var(--border);
|
|
1387
|
+
font-size: 11px;
|
|
1388
|
+
color: var(--text-faint);
|
|
1389
|
+
text-transform: uppercase;
|
|
1390
|
+
letter-spacing: 1px;
|
|
1391
|
+
font-weight: 600;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.editor-pane { border-right: 1px solid var(--border); }
|
|
1395
|
+
|
|
1396
|
+
#editor {
|
|
1397
|
+
flex: 1;
|
|
1398
|
+
width: 100%;
|
|
1399
|
+
background: var(--bg-primary);
|
|
1400
|
+
color: var(--text-primary);
|
|
1401
|
+
border: none;
|
|
1402
|
+
padding: 20px;
|
|
1403
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
1404
|
+
font-size: 14px;
|
|
1405
|
+
line-height: 1.7;
|
|
1406
|
+
resize: none;
|
|
1407
|
+
tab-size: 2;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
#editor:focus { outline: none; }
|
|
1411
|
+
|
|
1412
|
+
#preview {
|
|
1413
|
+
flex: 1;
|
|
1414
|
+
padding: 20px;
|
|
1415
|
+
overflow-y: auto;
|
|
1416
|
+
line-height: 1.7;
|
|
1417
|
+
background: var(--bg-primary);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
#preview h1 { font-size: 2em; margin-bottom: 16px; color: var(--text-primary); font-weight: 700; }
|
|
1421
|
+
#preview h2 { font-size: 1.5em; margin: 32px 0 12px; color: var(--text-primary); font-weight: 600; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
1422
|
+
#preview h3 { font-size: 1.25em; margin: 24px 0 8px; color: var(--text-primary); font-weight: 600; }
|
|
1423
|
+
#preview p { margin-bottom: 16px; color: var(--text-secondary); }
|
|
1424
|
+
#preview strong { color: var(--text-primary); font-weight: 600; }
|
|
1425
|
+
#preview em { color: var(--text-secondary); }
|
|
1426
|
+
#preview ul, #preview ol { margin: 16px 0; padding-left: 24px; color: var(--text-secondary); }
|
|
1427
|
+
#preview li { margin: 8px 0; }
|
|
1428
|
+
#preview code { background: var(--code-bg); padding: 3px 8px; border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--code-color); }
|
|
1429
|
+
#preview pre { background: var(--bg-secondary); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; border: 1px solid var(--border); }
|
|
1430
|
+
#preview pre code { background: none; padding: 0; color: var(--text-primary); }
|
|
1431
|
+
#preview blockquote { border-left: 3px solid #f59e0b; padding-left: 16px; color: var(--text-muted); margin: 16px 0; font-style: italic; }
|
|
1432
|
+
#preview a { color: #f59e0b; text-decoration: none; }
|
|
1433
|
+
#preview a:hover { text-decoration: underline; }
|
|
1434
|
+
|
|
1435
|
+
.toast {
|
|
1436
|
+
position: fixed;
|
|
1437
|
+
bottom: 24px;
|
|
1438
|
+
right: 24px;
|
|
1439
|
+
background: var(--bg-secondary);
|
|
1440
|
+
border: 1px solid var(--border);
|
|
1441
|
+
color: var(--text-primary);
|
|
1442
|
+
padding: 14px 20px;
|
|
1443
|
+
border-radius: 10px;
|
|
1444
|
+
font-size: 14px;
|
|
1445
|
+
opacity: 0;
|
|
1446
|
+
transform: translateY(10px);
|
|
1447
|
+
transition: all 0.2s;
|
|
1448
|
+
display: flex;
|
|
1449
|
+
align-items: center;
|
|
1450
|
+
gap: 10px;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.toast.show { opacity: 1; transform: translateY(0); }
|
|
1454
|
+
.toast.success { border-color: #22c55e; }
|
|
1455
|
+
.toast.success::before { content: ''; display: block; width: 8px; height: 8px; background: #22c55e; border-radius: 50%; }
|
|
1456
|
+
|
|
1457
|
+
.shortcut {
|
|
1458
|
+
font-size: 11px;
|
|
1459
|
+
color: var(--text-faint);
|
|
1460
|
+
background: var(--bg-tertiary);
|
|
1461
|
+
padding: 2px 6px;
|
|
1462
|
+
border-radius: 4px;
|
|
1463
|
+
margin-left: 4px;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
.symbol-dropdown { position: relative; }
|
|
1467
|
+
|
|
1468
|
+
.symbol-panel {
|
|
1469
|
+
display: none;
|
|
1470
|
+
position: absolute;
|
|
1471
|
+
top: 100%;
|
|
1472
|
+
left: 0;
|
|
1473
|
+
background: var(--bg-secondary);
|
|
1474
|
+
border: 1px solid var(--border);
|
|
1475
|
+
border-radius: 12px;
|
|
1476
|
+
padding: 16px;
|
|
1477
|
+
z-index: 100;
|
|
1478
|
+
width: 320px;
|
|
1479
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
1480
|
+
max-height: 400px;
|
|
1481
|
+
overflow-y: auto;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
.symbol-panel.show { display: block; }
|
|
1485
|
+
.symbol-category { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 1px; margin: 12px 0 8px; font-weight: 600; }
|
|
1486
|
+
.symbol-category:first-child { margin-top: 0; }
|
|
1487
|
+
.symbol-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; }
|
|
1488
|
+
.symbol-grid button { padding: 8px; font-size: 16px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); cursor: pointer; }
|
|
1489
|
+
.symbol-grid button:hover { background: #f59e0b; border-color: #f59e0b; color: white; transform: scale(1.1); }
|
|
1490
|
+
|
|
1491
|
+
.save-status {
|
|
1492
|
+
font-size: 12px;
|
|
1493
|
+
padding: 4px 10px;
|
|
1494
|
+
border-radius: 6px;
|
|
1495
|
+
margin-left: auto;
|
|
1496
|
+
margin-right: 8px;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.save-status.saved { color: #22c55e; background: rgba(34, 197, 94, 0.1); }
|
|
1500
|
+
.save-status.unsaved { color: #f59e0b; background: rgba(245, 158, 11, 0.1); }
|
|
1501
|
+
.save-status.saving { color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
|
|
1502
|
+
</style>
|
|
1503
|
+
</head>
|
|
1504
|
+
<body>
|
|
1505
|
+
<header>
|
|
1506
|
+
<div class="logo">Wasibase</div>
|
|
1507
|
+
<span class="edit-badge">Bearbeiten</span>
|
|
1508
|
+
<div class="breadcrumb">
|
|
1509
|
+
<span>${oberkategorie}</span>
|
|
1510
|
+
<span class="sep">/</span>
|
|
1511
|
+
<span>${unterkategorie}</span>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div class="header-right">
|
|
1514
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Theme wechseln" id="themeBtn">🌙</button>
|
|
1515
|
+
<span class="save-status" id="saveStatus"></span>
|
|
1516
|
+
<button class="btn-secondary" onclick="cancel()">Schliessen</button>
|
|
1517
|
+
<button class="btn-primary" id="saveBtn" onclick="save()">Speichern <span class="shortcut">⌘S</span></button>
|
|
1518
|
+
<button class="btn-done" id="doneBtn" onclick="done()">Beenden <span class="shortcut">Esc</span></button>
|
|
1519
|
+
</div>
|
|
1520
|
+
</header>
|
|
1521
|
+
|
|
1522
|
+
<div class="meta-bar">
|
|
1523
|
+
<div class="meta-field">
|
|
1524
|
+
<label>Thema</label>
|
|
1525
|
+
<input type="text" id="thema" value="${escapedThema}">
|
|
1526
|
+
</div>
|
|
1527
|
+
<input type="hidden" id="oberkategorie" value="${oberkategorie}">
|
|
1528
|
+
<input type="hidden" id="unterkategorie" value="${unterkategorie}">
|
|
1529
|
+
</div>
|
|
1530
|
+
|
|
1531
|
+
<div class="toolbar">
|
|
1532
|
+
<button onclick="insertFormat('**', '**')" title="Fett"><b>B</b></button>
|
|
1533
|
+
<button onclick="insertFormat('*', '*')" title="Kursiv"><i>I</i></button>
|
|
1534
|
+
<button onclick="insertFormat('~~', '~~')" title="Durchgestrichen"><s>S</s></button>
|
|
1535
|
+
<div class="sep"></div>
|
|
1536
|
+
<button onclick="insertLine('# ')">H1</button>
|
|
1537
|
+
<button onclick="insertLine('## ')">H2</button>
|
|
1538
|
+
<button onclick="insertLine('### ')">H3</button>
|
|
1539
|
+
<div class="sep"></div>
|
|
1540
|
+
<button onclick="insertLine('- ')">• Liste</button>
|
|
1541
|
+
<button onclick="insertLine('1. ')">1. Liste</button>
|
|
1542
|
+
<button onclick="insertLine('> ')">" Zitat</button>
|
|
1543
|
+
<div class="sep"></div>
|
|
1544
|
+
<button onclick="insertFormat('\`', '\`')"></></button>
|
|
1545
|
+
<button onclick="insertCodeBlock()">Code</button>
|
|
1546
|
+
<button onclick="insertFormat('[', '](url)')">Link</button>
|
|
1547
|
+
<div class="sep"></div>
|
|
1548
|
+
<button onclick="insertFormat('[[', ']]')">[[Link]]</button>
|
|
1549
|
+
<div class="sep"></div>
|
|
1550
|
+
<div class="symbol-dropdown">
|
|
1551
|
+
<button onclick="toggleSymbols()">∑ Math</button>
|
|
1552
|
+
<div class="symbol-panel" id="symbolPanel">
|
|
1553
|
+
<div class="symbol-category">Griechisch</div>
|
|
1554
|
+
<div class="symbol-grid">
|
|
1555
|
+
<button onclick="insertSymbol('α')">α</button>
|
|
1556
|
+
<button onclick="insertSymbol('β')">β</button>
|
|
1557
|
+
<button onclick="insertSymbol('γ')">γ</button>
|
|
1558
|
+
<button onclick="insertSymbol('δ')">δ</button>
|
|
1559
|
+
<button onclick="insertSymbol('ε')">ε</button>
|
|
1560
|
+
<button onclick="insertSymbol('θ')">θ</button>
|
|
1561
|
+
<button onclick="insertSymbol('λ')">λ</button>
|
|
1562
|
+
<button onclick="insertSymbol('μ')">μ</button>
|
|
1563
|
+
<button onclick="insertSymbol('π')">π</button>
|
|
1564
|
+
<button onclick="insertSymbol('σ')">σ</button>
|
|
1565
|
+
<button onclick="insertSymbol('φ')">φ</button>
|
|
1566
|
+
<button onclick="insertSymbol('ω')">ω</button>
|
|
1567
|
+
<button onclick="insertSymbol('Σ')">Σ</button>
|
|
1568
|
+
<button onclick="insertSymbol('Δ')">Δ</button>
|
|
1569
|
+
<button onclick="insertSymbol('Ω')">Ω</button>
|
|
1570
|
+
</div>
|
|
1571
|
+
<div class="symbol-category">Operatoren</div>
|
|
1572
|
+
<div class="symbol-grid">
|
|
1573
|
+
<button onclick="insertSymbol('±')">±</button>
|
|
1574
|
+
<button onclick="insertSymbol('×')">×</button>
|
|
1575
|
+
<button onclick="insertSymbol('÷')">÷</button>
|
|
1576
|
+
<button onclick="insertSymbol('∞')">∞</button>
|
|
1577
|
+
<button onclick="insertSymbol('√')">√</button>
|
|
1578
|
+
<button onclick="insertSymbol('∫')">∫</button>
|
|
1579
|
+
<button onclick="insertSymbol('∑')">∑</button>
|
|
1580
|
+
<button onclick="insertSymbol('≠')">≠</button>
|
|
1581
|
+
<button onclick="insertSymbol('≈')">≈</button>
|
|
1582
|
+
<button onclick="insertSymbol('≤')">≤</button>
|
|
1583
|
+
<button onclick="insertSymbol('≥')">≥</button>
|
|
1584
|
+
</div>
|
|
1585
|
+
<div class="symbol-category">Lineare Algebra</div>
|
|
1586
|
+
<div class="symbol-grid">
|
|
1587
|
+
<button onclick="insertSymbol('ᵀ')" title="Transponiert">ᵀ</button>
|
|
1588
|
+
<button onclick="insertSymbol('⁻¹')" title="Inverse">⁻¹</button>
|
|
1589
|
+
<button onclick="insertSymbol('det')" title="Determinante">det</button>
|
|
1590
|
+
<button onclick="insertSymbol('tr')" title="Spur">tr</button>
|
|
1591
|
+
<button onclick="insertSymbol('rk')" title="Rang">rk</button>
|
|
1592
|
+
<button onclick="insertSymbol('ker')" title="Kern">ker</button>
|
|
1593
|
+
<button onclick="insertSymbol('im')" title="Bild">im</button>
|
|
1594
|
+
<button onclick="insertSymbol('dim')" title="Dimension">dim</button>
|
|
1595
|
+
<button onclick="insertSymbol('span')" title="Aufspann">span</button>
|
|
1596
|
+
<button onclick="insertSymbol('⊥')" title="Orthogonal">⊥</button>
|
|
1597
|
+
<button onclick="insertSymbol('‖')" title="Norm">‖</button>
|
|
1598
|
+
<button onclick="insertSymbol('⟨')" title="Skalarprodukt"><</button>
|
|
1599
|
+
<button onclick="insertSymbol('⟩')" title="Skalarprodukt">></button>
|
|
1600
|
+
<button onclick="insertSymbol('⊗')" title="Tensorprodukt">⊗</button>
|
|
1601
|
+
<button onclick="insertSymbol('⊕')" title="Direkte Summe">⊕</button>
|
|
1602
|
+
<button onclick="insertSymbol('→')" title="Abbildung">→</button>
|
|
1603
|
+
</div>
|
|
1604
|
+
<div class="symbol-category">Hoch-/Tiefgestellt</div>
|
|
1605
|
+
<div class="symbol-grid">
|
|
1606
|
+
<button onclick="insertSymbol('⁰')">⁰</button>
|
|
1607
|
+
<button onclick="insertSymbol('¹')">¹</button>
|
|
1608
|
+
<button onclick="insertSymbol('²')">²</button>
|
|
1609
|
+
<button onclick="insertSymbol('³')">³</button>
|
|
1610
|
+
<button onclick="insertSymbol('ⁿ')">ⁿ</button>
|
|
1611
|
+
<button onclick="insertSymbol('₀')">₀</button>
|
|
1612
|
+
<button onclick="insertSymbol('₁')">₁</button>
|
|
1613
|
+
<button onclick="insertSymbol('₂')">₂</button>
|
|
1614
|
+
<button onclick="insertSymbol('ᵢ')">ᵢ</button>
|
|
1615
|
+
<button onclick="insertSymbol('ⱼ')">ⱼ</button>
|
|
1616
|
+
<button onclick="insertSymbol('ₙ')">ₙ</button>
|
|
1617
|
+
<button onclick="insertSymbol('ₘ')">ₘ</button>
|
|
1618
|
+
</div>
|
|
1619
|
+
</div>
|
|
1620
|
+
</div>
|
|
1621
|
+
</div>
|
|
1622
|
+
|
|
1623
|
+
<main>
|
|
1624
|
+
<div class="editor-pane">
|
|
1625
|
+
<div class="pane-header">Editor</div>
|
|
1626
|
+
<textarea id="editor">${escapedContent}</textarea>
|
|
1627
|
+
</div>
|
|
1628
|
+
<div class="preview-pane">
|
|
1629
|
+
<div class="pane-header">Vorschau</div>
|
|
1630
|
+
<div id="preview"></div>
|
|
1631
|
+
</div>
|
|
1632
|
+
</main>
|
|
1633
|
+
|
|
1634
|
+
<div class="toast" id="toast"></div>
|
|
1635
|
+
|
|
1636
|
+
<script>
|
|
1637
|
+
const editor = document.getElementById('editor');
|
|
1638
|
+
const preview = document.getElementById('preview');
|
|
1639
|
+
const themaInput = document.getElementById('thema');
|
|
1640
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
1641
|
+
const toast = document.getElementById('toast');
|
|
1642
|
+
|
|
1643
|
+
let lastSavedContent = editor.value;
|
|
1644
|
+
let lastSavedThema = themaInput.value;
|
|
1645
|
+
let autoSaveTimer;
|
|
1646
|
+
let hasUnsavedChanges = false;
|
|
1647
|
+
|
|
1648
|
+
// Initial preview
|
|
1649
|
+
updatePreview();
|
|
1650
|
+
|
|
1651
|
+
editor.addEventListener('input', () => {
|
|
1652
|
+
checkForChanges();
|
|
1653
|
+
clearTimeout(autoSaveTimer);
|
|
1654
|
+
autoSaveTimer = setTimeout(autoSave, 3000);
|
|
1655
|
+
setTimeout(updatePreview, 100);
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
themaInput.addEventListener('input', () => {
|
|
1659
|
+
checkForChanges();
|
|
1660
|
+
clearTimeout(autoSaveTimer);
|
|
1661
|
+
autoSaveTimer = setTimeout(autoSave, 3000);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
async function updatePreview() {
|
|
1665
|
+
const res = await fetch('/preview', {
|
|
1666
|
+
method: 'POST',
|
|
1667
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1668
|
+
body: JSON.stringify({ markdown: editor.value })
|
|
1669
|
+
});
|
|
1670
|
+
const { html } = await res.json();
|
|
1671
|
+
preview.innerHTML = DOMPurify.sanitize(html);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function insertFormat(before, after) {
|
|
1675
|
+
const start = editor.selectionStart;
|
|
1676
|
+
const end = editor.selectionEnd;
|
|
1677
|
+
const text = editor.value;
|
|
1678
|
+
const selected = text.substring(start, end) || 'text';
|
|
1679
|
+
editor.value = text.substring(0, start) + before + selected + after + text.substring(end);
|
|
1680
|
+
editor.focus();
|
|
1681
|
+
editor.selectionStart = start + before.length;
|
|
1682
|
+
editor.selectionEnd = start + before.length + selected.length;
|
|
1683
|
+
updatePreview();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function insertLine(prefix) {
|
|
1687
|
+
const start = editor.selectionStart;
|
|
1688
|
+
const text = editor.value;
|
|
1689
|
+
const lineStart = text.lastIndexOf('\\n', start - 1) + 1;
|
|
1690
|
+
editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
|
1691
|
+
editor.focus();
|
|
1692
|
+
editor.selectionStart = editor.selectionEnd = lineStart + prefix.length;
|
|
1693
|
+
updatePreview();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function insertCodeBlock() { insertFormat('\\n\`\`\`\\n', '\\n\`\`\`\\n'); }
|
|
1697
|
+
function toggleSymbols() { document.getElementById('symbolPanel').classList.toggle('show'); }
|
|
1698
|
+
function insertSymbol(symbol) {
|
|
1699
|
+
const start = editor.selectionStart;
|
|
1700
|
+
editor.value = editor.value.substring(0, start) + symbol + editor.value.substring(editor.selectionEnd);
|
|
1701
|
+
editor.focus();
|
|
1702
|
+
editor.selectionStart = editor.selectionEnd = start + symbol.length;
|
|
1703
|
+
updatePreview();
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
document.addEventListener('click', (e) => {
|
|
1707
|
+
const panel = document.getElementById('symbolPanel');
|
|
1708
|
+
if (!e.target.closest('.symbol-dropdown') && panel.classList.contains('show')) {
|
|
1709
|
+
panel.classList.remove('show');
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
function checkForChanges() {
|
|
1714
|
+
hasUnsavedChanges = (editor.value !== lastSavedContent || themaInput.value !== lastSavedThema);
|
|
1715
|
+
updateSaveStatus();
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function updateSaveStatus() {
|
|
1719
|
+
const status = document.getElementById('saveStatus');
|
|
1720
|
+
if (hasUnsavedChanges) {
|
|
1721
|
+
status.textContent = 'Ungespeichert';
|
|
1722
|
+
status.className = 'save-status unsaved';
|
|
1723
|
+
} else {
|
|
1724
|
+
status.textContent = 'Gespeichert';
|
|
1725
|
+
status.className = 'save-status saved';
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
async function autoSave() {
|
|
1730
|
+
const thema = themaInput.value.trim();
|
|
1731
|
+
if (!thema || !editor.value) return;
|
|
1732
|
+
if (editor.value === lastSavedContent && thema === lastSavedThema) return;
|
|
1733
|
+
|
|
1734
|
+
const status = document.getElementById('saveStatus');
|
|
1735
|
+
status.textContent = 'Speichert...';
|
|
1736
|
+
status.className = 'save-status saving';
|
|
1737
|
+
|
|
1738
|
+
try {
|
|
1739
|
+
const res = await fetch('/autosave', {
|
|
1740
|
+
method: 'POST',
|
|
1741
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1742
|
+
body: JSON.stringify({
|
|
1743
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1744
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1745
|
+
thema,
|
|
1746
|
+
content: editor.value
|
|
1747
|
+
})
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
const data = await res.json();
|
|
1751
|
+
if (data.success) {
|
|
1752
|
+
lastSavedContent = editor.value;
|
|
1753
|
+
lastSavedThema = thema;
|
|
1754
|
+
hasUnsavedChanges = false;
|
|
1755
|
+
status.textContent = 'Gespeichert ' + data.savedAt;
|
|
1756
|
+
status.className = 'save-status saved';
|
|
1757
|
+
}
|
|
1758
|
+
} catch (e) { console.error('Auto-save failed:', e); }
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async function save() {
|
|
1762
|
+
const thema = themaInput.value.trim();
|
|
1763
|
+
if (!thema) { showToast('Bitte Thema eingeben', 'error'); return false; }
|
|
1764
|
+
|
|
1765
|
+
const res = await fetch('/autosave', {
|
|
1766
|
+
method: 'POST',
|
|
1767
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1768
|
+
body: JSON.stringify({
|
|
1769
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1770
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1771
|
+
thema,
|
|
1772
|
+
content: editor.value
|
|
1773
|
+
})
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
const data = await res.json();
|
|
1777
|
+
if (data.success) {
|
|
1778
|
+
lastSavedContent = editor.value;
|
|
1779
|
+
lastSavedThema = thema;
|
|
1780
|
+
hasUnsavedChanges = false;
|
|
1781
|
+
showToast('Gespeichert!', 'success');
|
|
1782
|
+
updateSaveStatus();
|
|
1783
|
+
return true;
|
|
1784
|
+
}
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
async function done() {
|
|
1789
|
+
if (themaInput.value.trim() && editor.value) await save();
|
|
1790
|
+
await fetch('/cancel', { method: 'POST' });
|
|
1791
|
+
window.close();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async function cancel() {
|
|
1795
|
+
await fetch('/cancel', { method: 'POST' });
|
|
1796
|
+
window.close();
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function showToast(message, type = '') {
|
|
1800
|
+
toast.textContent = message;
|
|
1801
|
+
toast.className = 'toast show ' + type;
|
|
1802
|
+
setTimeout(() => toast.className = 'toast', 3000);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
document.addEventListener('keydown', (e) => {
|
|
1806
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); save(); }
|
|
1807
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'b') { e.preventDefault(); insertFormat('**', '**'); }
|
|
1808
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'i') { e.preventDefault(); insertFormat('*', '*'); }
|
|
1809
|
+
if (e.key === 'Escape') done();
|
|
1810
|
+
if (e.key === 'Tab' && document.activeElement === editor) {
|
|
1811
|
+
e.preventDefault();
|
|
1812
|
+
const start = editor.selectionStart;
|
|
1813
|
+
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(editor.selectionEnd);
|
|
1814
|
+
editor.selectionStart = editor.selectionEnd = start + 2;
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
window.addEventListener('beforeunload', (e) => {
|
|
1819
|
+
if (hasUnsavedChanges && themaInput.value.trim()) {
|
|
1820
|
+
// Try to save synchronously before page unloads
|
|
1821
|
+
navigator.sendBeacon('/autosave', JSON.stringify({
|
|
1822
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1823
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1824
|
+
thema: themaInput.value.trim(),
|
|
1825
|
+
content: editor.value
|
|
1826
|
+
}));
|
|
1827
|
+
e.preventDefault();
|
|
1828
|
+
e.returnValue = '';
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
// Save when tab becomes hidden
|
|
1833
|
+
document.addEventListener('visibilitychange', () => {
|
|
1834
|
+
if (document.hidden && themaInput.value.trim() && editor.value) {
|
|
1835
|
+
autoSave();
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
// Heartbeat - send current state to server every 3 seconds
|
|
1840
|
+
// Also checks if server is still alive - closes window if not
|
|
1841
|
+
let serverCheckFailures = 0;
|
|
1842
|
+
setInterval(async () => {
|
|
1843
|
+
try {
|
|
1844
|
+
const res = await fetch('/heartbeat', {
|
|
1845
|
+
method: 'POST',
|
|
1846
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1847
|
+
body: JSON.stringify({
|
|
1848
|
+
oberkategorie: document.getElementById('oberkategorie').value,
|
|
1849
|
+
unterkategorie: document.getElementById('unterkategorie').value,
|
|
1850
|
+
thema: themaInput.value.trim(),
|
|
1851
|
+
content: editor.value
|
|
1852
|
+
})
|
|
1853
|
+
});
|
|
1854
|
+
if (res.ok) {
|
|
1855
|
+
serverCheckFailures = 0;
|
|
1856
|
+
} else {
|
|
1857
|
+
serverCheckFailures++;
|
|
1858
|
+
}
|
|
1859
|
+
} catch (e) {
|
|
1860
|
+
serverCheckFailures++;
|
|
1861
|
+
if (serverCheckFailures >= 2) {
|
|
1862
|
+
// Server is gone, close the window
|
|
1863
|
+
window.close();
|
|
1864
|
+
// Fallback: show message if window.close() doesn't work
|
|
1865
|
+
document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;flex-direction:column;background:#0a0a0a;color:#666;font-family:system-ui;"><h2 style="color:#fff;margin-bottom:16px;">Server beendet</h2><p>Du kannst dieses Fenster schliessen.</p></div>';
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}, 3000);
|
|
1869
|
+
|
|
1870
|
+
function toggleTheme() {
|
|
1871
|
+
const html = document.documentElement;
|
|
1872
|
+
const btn = document.getElementById('themeBtn');
|
|
1873
|
+
if (html.getAttribute('data-theme') === 'light') {
|
|
1874
|
+
html.removeAttribute('data-theme');
|
|
1875
|
+
btn.textContent = '🌙';
|
|
1876
|
+
localStorage.setItem('wasibase-theme', 'dark');
|
|
1877
|
+
} else {
|
|
1878
|
+
html.setAttribute('data-theme', 'light');
|
|
1879
|
+
btn.textContent = '☀️';
|
|
1880
|
+
localStorage.setItem('wasibase-theme', 'light');
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
(function() {
|
|
1885
|
+
const savedTheme = localStorage.getItem('wasibase-theme');
|
|
1886
|
+
if (savedTheme === 'light') {
|
|
1887
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1888
|
+
document.getElementById('themeBtn').textContent = '☀️';
|
|
1889
|
+
}
|
|
1890
|
+
updateSaveStatus();
|
|
1891
|
+
})();
|
|
1892
|
+
</script>
|
|
1893
|
+
</body>
|
|
1894
|
+
</html>`;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function getSearchHTML() {
|
|
1898
|
+
return `<!DOCTYPE html>
|
|
1899
|
+
<html lang="de">
|
|
1900
|
+
<head>
|
|
1901
|
+
<meta charset="UTF-8">
|
|
1902
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1903
|
+
<title>Wasibase - Suche</title>
|
|
1904
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
|
1905
|
+
<style>
|
|
1906
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1907
|
+
|
|
1908
|
+
body {
|
|
1909
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
1910
|
+
background: #0a0a0a;
|
|
1911
|
+
color: #e5e5e5;
|
|
1912
|
+
min-height: 100vh;
|
|
1913
|
+
padding: 40px;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
.container {
|
|
1917
|
+
max-width: 800px;
|
|
1918
|
+
margin: 0 auto;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
header {
|
|
1922
|
+
display: flex;
|
|
1923
|
+
align-items: center;
|
|
1924
|
+
justify-content: space-between;
|
|
1925
|
+
margin-bottom: 32px;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.logo {
|
|
1929
|
+
font-size: 24px;
|
|
1930
|
+
font-weight: 700;
|
|
1931
|
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
|
1932
|
+
-webkit-background-clip: text;
|
|
1933
|
+
-webkit-text-fill-color: transparent;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.close-btn {
|
|
1937
|
+
background: #262626;
|
|
1938
|
+
color: #a3a3a3;
|
|
1939
|
+
border: none;
|
|
1940
|
+
padding: 8px 16px;
|
|
1941
|
+
border-radius: 8px;
|
|
1942
|
+
cursor: pointer;
|
|
1943
|
+
font-size: 14px;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.close-btn:hover { background: #333; color: #e5e5e5; }
|
|
1947
|
+
|
|
1948
|
+
.search-box {
|
|
1949
|
+
position: relative;
|
|
1950
|
+
margin-bottom: 32px;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
#searchInput {
|
|
1954
|
+
width: 100%;
|
|
1955
|
+
background: #141414;
|
|
1956
|
+
border: 1px solid #262626;
|
|
1957
|
+
border-radius: 12px;
|
|
1958
|
+
padding: 16px 20px 16px 48px;
|
|
1959
|
+
color: #e5e5e5;
|
|
1960
|
+
font-size: 16px;
|
|
1961
|
+
transition: all 0.15s;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
#searchInput:focus {
|
|
1965
|
+
outline: none;
|
|
1966
|
+
border-color: #3b82f6;
|
|
1967
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
#searchInput::placeholder { color: #525252; }
|
|
1971
|
+
|
|
1972
|
+
.search-icon {
|
|
1973
|
+
position: absolute;
|
|
1974
|
+
left: 16px;
|
|
1975
|
+
top: 50%;
|
|
1976
|
+
transform: translateY(-50%);
|
|
1977
|
+
color: #525252;
|
|
1978
|
+
font-size: 18px;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.results {
|
|
1982
|
+
display: flex;
|
|
1983
|
+
flex-direction: column;
|
|
1984
|
+
gap: 12px;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
.result-item {
|
|
1988
|
+
background: #141414;
|
|
1989
|
+
border: 1px solid #262626;
|
|
1990
|
+
border-radius: 12px;
|
|
1991
|
+
padding: 16px 20px;
|
|
1992
|
+
cursor: pointer;
|
|
1993
|
+
transition: all 0.15s;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
.result-item:hover {
|
|
1997
|
+
border-color: #3b82f6;
|
|
1998
|
+
transform: translateX(4px);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
.result-path {
|
|
2002
|
+
font-size: 12px;
|
|
2003
|
+
color: #525252;
|
|
2004
|
+
margin-bottom: 6px;
|
|
2005
|
+
display: flex;
|
|
2006
|
+
gap: 6px;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
.result-path span { color: #737373; }
|
|
2010
|
+
|
|
2011
|
+
.result-title {
|
|
2012
|
+
font-size: 16px;
|
|
2013
|
+
font-weight: 600;
|
|
2014
|
+
color: #e5e5e5;
|
|
2015
|
+
margin-bottom: 8px;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
.result-preview {
|
|
2019
|
+
font-size: 14px;
|
|
2020
|
+
color: #737373;
|
|
2021
|
+
line-height: 1.5;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
.result-preview mark {
|
|
2025
|
+
background: rgba(59, 130, 246, 0.3);
|
|
2026
|
+
color: #93c5fd;
|
|
2027
|
+
padding: 1px 4px;
|
|
2028
|
+
border-radius: 3px;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
.no-results {
|
|
2032
|
+
text-align: center;
|
|
2033
|
+
color: #525252;
|
|
2034
|
+
padding: 60px 20px;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
.no-results .icon { font-size: 48px; margin-bottom: 16px; }
|
|
2038
|
+
|
|
2039
|
+
.stats {
|
|
2040
|
+
font-size: 13px;
|
|
2041
|
+
color: #525252;
|
|
2042
|
+
margin-bottom: 16px;
|
|
2043
|
+
}
|
|
2044
|
+
</style>
|
|
2045
|
+
</head>
|
|
2046
|
+
<body>
|
|
2047
|
+
<div class="container">
|
|
2048
|
+
<header>
|
|
2049
|
+
<div class="logo">Wasibase Suche</div>
|
|
2050
|
+
<button class="close-btn" onclick="closeSearch()">Schliessen (Esc)</button>
|
|
2051
|
+
</header>
|
|
2052
|
+
|
|
2053
|
+
<div class="search-box">
|
|
2054
|
+
<span class="search-icon">🔍</span>
|
|
2055
|
+
<input type="text" id="searchInput" placeholder="Notes durchsuchen..." autofocus>
|
|
2056
|
+
</div>
|
|
2057
|
+
|
|
2058
|
+
<div class="stats" id="stats"></div>
|
|
2059
|
+
<div class="results" id="results"></div>
|
|
2060
|
+
</div>
|
|
2061
|
+
|
|
2062
|
+
<script>
|
|
2063
|
+
const searchInput = document.getElementById('searchInput');
|
|
2064
|
+
const resultsDiv = document.getElementById('results');
|
|
2065
|
+
const statsDiv = document.getElementById('stats');
|
|
2066
|
+
|
|
2067
|
+
let debounceTimer;
|
|
2068
|
+
|
|
2069
|
+
searchInput.addEventListener('input', () => {
|
|
2070
|
+
clearTimeout(debounceTimer);
|
|
2071
|
+
debounceTimer = setTimeout(search, 200);
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
async function search() {
|
|
2075
|
+
const query = searchInput.value.trim();
|
|
2076
|
+
if (!query) {
|
|
2077
|
+
resultsDiv.innerHTML = '';
|
|
2078
|
+
statsDiv.textContent = '';
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const res = await fetch('/search', {
|
|
2083
|
+
method: 'POST',
|
|
2084
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2085
|
+
body: JSON.stringify({ query })
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
const { results } = await res.json();
|
|
2089
|
+
|
|
2090
|
+
if (results.length === 0) {
|
|
2091
|
+
statsDiv.textContent = '';
|
|
2092
|
+
resultsDiv.innerHTML = '<div class="no-results"><div class="icon">🔎</div>Keine Ergebnisse gefunden</div>';
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
statsDiv.textContent = results.length + ' Ergebnis' + (results.length !== 1 ? 'se' : '') + ' gefunden';
|
|
2097
|
+
|
|
2098
|
+
resultsDiv.innerHTML = results.map(r => {
|
|
2099
|
+
const previewText = DOMPurify.sanitize(r.preview);
|
|
2100
|
+
return '<div class="result-item" onclick="openNote(\\'' + encodeURIComponent(JSON.stringify(r)) + '\\')">' +
|
|
2101
|
+
'<div class="result-path"><span>' + escapeHtml(r.oberkategorie) + '</span> / <span>' + escapeHtml(r.unterkategorie) + '</span></div>' +
|
|
2102
|
+
'<div class="result-title">' + escapeHtml(r.thema) + '</div>' +
|
|
2103
|
+
'<div class="result-preview">' + previewText + '</div>' +
|
|
2104
|
+
'</div>';
|
|
2105
|
+
}).join('');
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function escapeHtml(text) {
|
|
2109
|
+
const div = document.createElement('div');
|
|
2110
|
+
div.textContent = text;
|
|
2111
|
+
return div.innerHTML;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function openNote(encoded) {
|
|
2115
|
+
const note = JSON.parse(decodeURIComponent(encoded));
|
|
2116
|
+
alert('Oeffne: ' + note.oberkategorie + ' / ' + note.unterkategorie + ' / ' + note.thema);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async function closeSearch() {
|
|
2120
|
+
await fetch('/close', { method: 'POST' });
|
|
2121
|
+
window.close();
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
document.addEventListener('keydown', (e) => {
|
|
2125
|
+
if (e.key === 'Escape') closeSearch();
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
searchInput.focus();
|
|
2129
|
+
</script>
|
|
2130
|
+
</body>
|
|
2131
|
+
</html>`;
|
|
2132
|
+
}
|