File Explorer: CodeMirror + EasyMDE statt Preview

This commit is contained in:
Meleki committed 2026-02-28 11:30:26 +01:00
1 parent b4521a95ef
commit bba82aad7e
18 files changed
+410 -87

No files matched your search

+140 -48
View File
@@ -59,6 +59,7 @@ function openFileExplorer(opts = {}) {
}
function closeFileExplorer() {
feDestroyEditor();
const overlay = document.getElementById('feOverlay');
if (overlay) overlay.remove();
if (_feState && _feState._keyHandler) {
@@ -83,6 +84,7 @@ async function feLoadDir(path) {
sidebar.innerHTML = '<div class="fe-loading">Laden...</div>';
// Preview zurücksetzen
feDestroyEditor();
const main = document.getElementById('feMain');
main.innerHTML = '<div class="fe-placeholder">Datei auswählen</div>';
document.getElementById('feFooter').hidden = true;
@@ -163,6 +165,9 @@ async function feSelectFile(entry, itemEl) {
const footer = document.getElementById('feFooter');
const info = document.getElementById('feInfo');
// Vorherige Editor-Instanz aufräumen
feDestroyEditor();
if (!entry.isText) {
main.innerHTML = `<div class="fe-placeholder">${getFileIcon(entry.ext)} ${entry.name}<br><small>${formatSize(entry.size)} - Vorschau nicht verfügbar</small></div>`;
footer.hidden = true;
@@ -182,30 +187,15 @@ async function feSelectFile(entry, itemEl) {
const data = await resp.json();
_feState.selectedFile = data;
// Textarea mit Zeilennummern
main.innerHTML = `
<div class="fe-preview">
<div class="fe-line-numbers" id="feLineNums"></div>
<textarea class="fe-textarea" id="feTextarea" readonly spellcheck="false">${escHtml(data.content)}</textarea>
</div>
`;
const isMarkdown = entry.ext === '.md' || entry.ext === '.markdown';
const textarea = document.getElementById('feTextarea');
const lineNums = document.getElementById('feLineNums');
if (isMarkdown) {
feInitEasyMDE(main, data);
} else {
feInitCodeMirror(main, data, entry.ext);
}
// Zeilennummern generieren
renderLineNumbers(data.content, lineNums);
// Sync scroll
textarea.addEventListener('scroll', () => {
lineNums.scrollTop = textarea.scrollTop;
});
// Selection-Tracking für Zeilen-Info
textarea.addEventListener('mouseup', () => updateSelectionInfo());
textarea.addEventListener('keyup', () => updateSelectionInfo());
info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
footer.hidden = false;
} catch (e) {
@@ -213,42 +203,144 @@ async function feSelectFile(entry, itemEl) {
}
}
function renderLineNumbers(content, container) {
const lines = content.split('\n');
container.innerHTML = lines.map((_, i) => `<div>${i + 1}</div>`).join('');
}
function updateSelectionInfo() {
if (!_feState || !_feState.selectedFile) return;
const textarea = document.getElementById('feTextarea');
const info = document.getElementById('feInfo');
const data = _feState.selectedFile;
if (textarea.selectionStart !== textarea.selectionEnd) {
const from = getLineNumber(textarea.value, textarea.selectionStart);
const to = getLineNumber(textarea.value, textarea.selectionEnd);
info.textContent = `${data.path}:${from}-${to} (${to - from + 1} Zeilen ausgewählt)`;
} else {
info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
function feDestroyEditor() {
if (_feState && _feState._editor) {
if (_feState._editorType === 'easymde') {
try { _feState._editor.toTextArea(); } catch (_) {}
}
// CodeMirror wird durch innerHTML-Reset automatisch verworfen
_feState._editor = null;
_feState._editorType = null;
}
}
function getLineNumber(text, pos) {
return text.substring(0, pos).split('\n').length;
function feInitCodeMirror(container, data, ext) {
container.innerHTML = '<div class="fe-cm-wrap"><textarea id="feCmTextarea"></textarea></div>';
const textarea = document.getElementById('feCmTextarea');
textarea.value = data.content;
const mode = getCmMode(ext);
const cm = CodeMirror.fromTextArea(textarea, {
mode: mode,
theme: 'default',
lineNumbers: true,
readOnly: true,
lineWrapping: false,
tabSize: 4,
indentWithTabs: false,
extraKeys: {}
});
// Selektion-Tracking für Zeilen-Info
cm.on('cursorActivity', () => {
if (!_feState || !_feState.selectedFile) return;
feUpdateSelectionInfo(cm, data);
});
_feState._editor = cm;
_feState._editorType = 'codemirror';
}
function feInitEasyMDE(container, data) {
container.innerHTML = '<div class="fe-mde-wrap"><textarea id="feMdeTextarea"></textarea></div>';
const textarea = document.getElementById('feMdeTextarea');
const mde = new EasyMDE({
element: textarea,
initialValue: data.content,
toolbar: false,
status: false,
autofocus: false,
spellChecker: false,
autoDownloadFontAwesome: false,
minHeight: '100%',
maxHeight: '100%'
});
// Readonly via CodeMirror-Instanz
mde.codemirror.setOption('readOnly', true);
mde.codemirror.setOption('theme', 'default');
mde.codemirror.on('cursorActivity', () => {
if (!_feState || !_feState.selectedFile) return;
feUpdateSelectionInfo(mde.codemirror, data);
});
_feState._editor = mde;
_feState._editorType = 'easymde';
}
function getCmMode(ext) {
const map = {
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
'.ts': 'javascript', '.jsx': 'javascript', '.tsx': 'javascript',
'.json': 'javascript',
'.css': 'css', '.scss': 'css', '.less': 'css',
'.html': 'htmlmixed', '.htm': 'htmlmixed',
'.xml': 'xml', '.svg': 'xml', '.csproj': 'xml', '.sln': 'xml',
'.py': 'python',
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
'.yaml': 'yaml', '.yml': 'yaml',
'.cs': 'clike', '.java': 'clike', '.cpp': 'clike', '.c': 'clike', '.h': 'clike'
};
return map[ext?.toLowerCase()] || 'null';
}
function feGetCm() {
if (!_feState || !_feState._editor) return null;
if (_feState._editorType === 'easymde') return _feState._editor.codemirror;
if (_feState._editorType === 'codemirror') return _feState._editor;
return null;
}
function feGetSelection(cm) {
const sel = cm.listSelections()[0];
const anchorLine = sel.anchor.line;
const headLine = sel.head.line;
const anchorCh = sel.anchor.ch;
const headCh = sel.head.ch;
// Echte Selektion: unterschiedliche Position (line oder ch)
const hasSelection = anchorLine !== headLine || anchorCh !== headCh;
if (!hasSelection) return null;
const fromLine = Math.min(anchorLine, headLine) + 1;
const toLine = Math.max(anchorLine, headLine) + 1;
return { from: fromLine, to: toLine };
}
function feUpdateSelectionInfo(cm, data) {
const info = document.getElementById('feInfo');
if (!info) return;
const sel = feGetSelection(cm);
if (sel && sel.from !== sel.to) {
info.textContent = `${data.path}:${sel.from}-${sel.to} (${sel.to - sel.from + 1} Zeilen ausgewählt)`;
} else if (sel) {
info.textContent = `${data.path}:${sel.from} (1 Zeile ausgewählt)`;
} else {
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
}
}
function feDoSelect() {
if (!_feState || !_feState.selectedFile) return;
const data = _feState.selectedFile;
const textarea = document.getElementById('feTextarea');
let ref = data.path;
if (textarea && textarea.selectionStart !== textarea.selectionEnd) {
const from = getLineNumber(textarea.value, textarea.selectionStart);
const to = getLineNumber(textarea.value, textarea.selectionEnd);
ref = `${data.path}:${from}-${to}`;
const cm = feGetCm();
if (cm) {
const sel = feGetSelection(cm);
if (sel) {
ref = sel.from === sel.to
? `${data.path}:${sel.from}`
: `${data.path}:${sel.from}-${sel.to}`;
}
}
if (_feState.onSelect) {