u
This commit is contained in:
@@ -27,7 +27,6 @@ import {
|
|||||||
formatDate,
|
formatDate,
|
||||||
loadParties,
|
loadParties,
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
numberDisplay,
|
|
||||||
partyDisplayName,
|
partyDisplayName,
|
||||||
} from './manifestConfig';
|
} from './manifestConfig';
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ class QuoteRow extends PureComponent {
|
|||||||
<ListItemButton disabled divider sx={rowSx}>
|
<ListItemButton disabled divider sx={rowSx}>
|
||||||
<Box sx={{ width: 120, flexShrink: 0 }}>
|
<Box sx={{ width: 120, flexShrink: 0 }}>
|
||||||
<Skeleton variant="text" width={80} height={20} />
|
<Skeleton variant="text" width={80} height={20} />
|
||||||
<Skeleton variant="text" width={50} height={16} />
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ width: 100, flexShrink: 0 }}>
|
<Box sx={{ width: 100, flexShrink: 0 }}>
|
||||||
<Skeleton variant="text" width={70} height={20} />
|
<Skeleton variant="text" width={70} height={20} />
|
||||||
@@ -125,9 +123,6 @@ class QuoteRow extends PureComponent {
|
|||||||
<Typography variant="body1" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
<Typography variant="body1" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
||||||
{quote.number}
|
{quote.number}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
|
|
||||||
{numberDisplay(quote.externalNumber)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ width: 100, flexShrink: 0, minWidth: 0 }}>
|
<Box sx={{ width: 100, flexShrink: 0, minWidth: 0 }}>
|
||||||
@@ -593,7 +588,7 @@ export class QuoteList extends PureComponent {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ width: 120, flexShrink: 0 }}>Nr. / Ref.</Box>
|
<Box sx={{ width: 120, flexShrink: 0 }}>Nr.</Box>
|
||||||
<Box sx={{ width: 100, flexShrink: 0 }}>Datum</Box>
|
<Box sx={{ width: 100, flexShrink: 0 }}>Datum</Box>
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>Kunde</Box>
|
<Box sx={{ flex: 1, minWidth: 0 }}>Kunde</Box>
|
||||||
<Box sx={{ width: 40, flexShrink: 0, textAlign: 'center' }}>Pos.</Box>
|
<Box sx={{ width: 40, flexShrink: 0, textAlign: 'center' }}>Pos.</Box>
|
||||||
|
|||||||
@@ -5,22 +5,124 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
import SaveRoundedIcon from '@mui/icons-material/SaveRounded';
|
import SaveRoundedIcon from '@mui/icons-material/SaveRounded';
|
||||||
import UndoRoundedIcon from '@mui/icons-material/UndoRounded';
|
import UndoRoundedIcon from '@mui/icons-material/UndoRounded';
|
||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
import Alert from '@mui/material/Alert';
|
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded';
|
||||||
|
|
||||||
|
import CodeMirror, { EditorView, keymap, scrollPastEnd } from '@uiw/react-codemirror';
|
||||||
|
import { indentWithTab } from '@codemirror/commands';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { css } from '@codemirror/lang-css';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
|
import { sql } from '@codemirror/lang-sql';
|
||||||
|
import { xml } from '@codemirror/lang-xml';
|
||||||
|
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
import { formatVfsSize } from './vfsUtils';
|
import { formatVfsSize } from './vfsUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CodeMirror language extension and human-readable language label
|
||||||
|
* based on fileName extension and MIME type.
|
||||||
|
*/
|
||||||
|
function getLanguageInfo(fileName, mime) {
|
||||||
|
if (!fileName && !mime) return { extension: null, label: 'Plain Text' };
|
||||||
|
const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : '';
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
case 'mjs':
|
||||||
|
case 'cjs':
|
||||||
|
return { extension: javascript({ jsx: true }), label: 'JavaScript' };
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
return { extension: javascript({ jsx: true, typescript: true }), label: 'TypeScript' };
|
||||||
|
case 'json':
|
||||||
|
case 'json5':
|
||||||
|
return { extension: json(), label: 'JSON' };
|
||||||
|
case 'html':
|
||||||
|
case 'htm':
|
||||||
|
return { extension: html(), label: 'HTML' };
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
case 'less':
|
||||||
|
return { extension: css(), label: 'CSS' };
|
||||||
|
case 'md':
|
||||||
|
case 'markdown':
|
||||||
|
return { extension: markdown(), label: 'Markdown' };
|
||||||
|
case 'sql':
|
||||||
|
return { extension: sql(), label: 'SQL' };
|
||||||
|
case 'xml':
|
||||||
|
case 'svg':
|
||||||
|
return { extension: xml(), label: ext === 'svg' ? 'SVG' : 'XML' };
|
||||||
|
default:
|
||||||
|
if (mime?.includes('json')) return { extension: json(), label: 'JSON' };
|
||||||
|
if (mime?.includes('javascript')) return { extension: javascript({ jsx: true }), label: 'JavaScript' };
|
||||||
|
if (mime?.includes('html')) return { extension: html(), label: 'HTML' };
|
||||||
|
if (mime?.includes('xml') || mime?.includes('svg')) return { extension: xml(), label: 'XML' };
|
||||||
|
if (mime?.includes('markdown')) return { extension: markdown(), label: 'Markdown' };
|
||||||
|
return { extension: null, label: 'Plain Text' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Softer dark theme for the editor code area (One Dark slate #282c34 / #21252b)
|
||||||
|
const EDITOR_BG = '#282c34';
|
||||||
|
const GUTTER_BG = '#21252b';
|
||||||
|
|
||||||
|
const customEditorTheme = EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||||
|
backgroundColor: `${EDITOR_BG} !important`,
|
||||||
|
color: '#abb2bf',
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
overflow: 'auto !important',
|
||||||
|
height: '100% !important',
|
||||||
|
maxHeight: '100%',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
lineHeight: '21px',
|
||||||
|
},
|
||||||
|
'.cm-content': {
|
||||||
|
padding: '10px 0',
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: `${GUTTER_BG} !important`,
|
||||||
|
color: '#636d83 !important',
|
||||||
|
borderRight: '1px solid rgba(255, 255, 255, 0.08) !important',
|
||||||
|
},
|
||||||
|
'.cm-lineNumbers .cm-gutterElement': {
|
||||||
|
padding: '0 10px 0 16px',
|
||||||
|
minWidth: '40px',
|
||||||
|
},
|
||||||
|
'.cm-activeLine': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.04) !important',
|
||||||
|
},
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.07) !important',
|
||||||
|
color: '#abb2bf !important',
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default class VfsTextEditor extends PureComponent {
|
export default class VfsTextEditor extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.textareaRef = React.createRef();
|
this.editorView = null;
|
||||||
this.gutterRef = React.createRef();
|
|
||||||
this.state = {
|
this.state = {
|
||||||
wrapLines: true,
|
wrapLines: true,
|
||||||
cursorLine: 1,
|
cursorLine: 1,
|
||||||
@@ -38,57 +140,47 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusEditor = () => {
|
handleEditorCreate = (view) => {
|
||||||
setTimeout(() => {
|
this.editorView = view;
|
||||||
this.textareaRef.current?.focus();
|
this.focusEditor();
|
||||||
}, 50);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleScroll = () => {
|
focusEditor = () => {
|
||||||
if (this.textareaRef.current && this.gutterRef.current) {
|
setTimeout(() => {
|
||||||
this.gutterRef.current.scrollTop = this.textareaRef.current.scrollTop;
|
this.editorView?.focus();
|
||||||
}
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleWrap = () => {
|
toggleWrap = () => {
|
||||||
this.setState((prev) => ({ wrapLines: !prev.wrapLines }));
|
this.setState((prev) => ({ wrapLines: !prev.wrapLines }));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCursorMove = () => {
|
handleViewUpdate = (viewUpdate) => {
|
||||||
const ta = this.textareaRef.current;
|
if (viewUpdate.selectionSet || viewUpdate.docChanged) {
|
||||||
if (!ta) return;
|
const state = viewUpdate.state;
|
||||||
const pos = ta.selectionStart || 0;
|
const head = state.selection.main.head;
|
||||||
const textBefore = (this.props.content || '').substring(0, pos);
|
const line = state.doc.lineAt(head);
|
||||||
const lines = textBefore.split('\n');
|
const cursorLine = line.number;
|
||||||
const line = lines.length;
|
const cursorCol = head - line.from + 1;
|
||||||
const col = lines[lines.length - 1].length + 1;
|
if (this.state.cursorLine !== cursorLine || this.state.cursorCol !== cursorCol) {
|
||||||
if (this.state.cursorLine !== line || this.state.cursorCol !== col) {
|
this.setState({ cursorLine, cursorCol });
|
||||||
this.setState({ cursorLine: line, cursorCol: col });
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMentionInChat = () => {
|
handleMentionInChat = () => {
|
||||||
const ta = this.textareaRef.current;
|
const view = this.editorView;
|
||||||
let lineInfo = null;
|
let lineInfo = null;
|
||||||
if (ta) {
|
if (view) {
|
||||||
const start = ta.selectionStart;
|
const { from, to } = view.state.selection.main;
|
||||||
const end = ta.selectionEnd;
|
if (typeof from === 'number' && typeof to === 'number' && to > from) {
|
||||||
if (typeof start === 'number' && typeof end === 'number' && end > start) {
|
const startLine = view.state.doc.lineAt(from).number;
|
||||||
const content = this.props.content || '';
|
const endLine = view.state.doc.lineAt(to).number;
|
||||||
const beforeStart = content.substring(0, start);
|
const selectedText = view.state.sliceDoc(from, to);
|
||||||
const startLine = beforeStart.split('\n').length;
|
|
||||||
|
|
||||||
let effectiveEnd = end;
|
|
||||||
if (effectiveEnd > start && content[effectiveEnd - 1] === '\n') {
|
|
||||||
effectiveEnd -= 1;
|
|
||||||
}
|
|
||||||
const beforeEnd = content.substring(0, effectiveEnd);
|
|
||||||
const endLine = Math.max(startLine, beforeEnd.split('\n').length);
|
|
||||||
|
|
||||||
lineInfo = {
|
lineInfo = {
|
||||||
startLine,
|
startLine,
|
||||||
endLine,
|
endLine,
|
||||||
selectedText: content.substring(start, end),
|
selectedText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,49 +190,17 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
|
||||||
// Ctrl+S or Cmd+S -> Save
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.props.isDirty && !this.props.isSaving) {
|
|
||||||
this.props.onSave();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab key -> Insert 2 spaces
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
const ta = this.textareaRef.current;
|
|
||||||
if (!ta) return;
|
|
||||||
const start = ta.selectionStart;
|
|
||||||
const end = ta.selectionEnd;
|
|
||||||
const val = this.props.content;
|
|
||||||
const next = val.substring(0, start) + ' ' + val.substring(end);
|
|
||||||
this.props.onChange(next);
|
|
||||||
setTimeout(() => {
|
|
||||||
ta.selectionStart = ta.selectionEnd = start + 2;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRevert = () => {
|
handleRevert = () => {
|
||||||
if (window.confirm('Möchtest du alle ungespeicherten Änderungen verwerfen?')) {
|
if (window.confirm('Möchtest du alle ungespeicherten Änderungen verwerfen?')) {
|
||||||
this.props.onChange(this.props.originalContent);
|
this.props.onChange(this.props.originalContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLineNumbers(lineCount) {
|
handleSaveShortcut = () => {
|
||||||
const lines = [];
|
if (this.props.isDirty && !this.props.isSaving) {
|
||||||
for (let i = 1; i <= lineCount; i++) {
|
this.props.onSave?.();
|
||||||
lines.push(
|
|
||||||
<div key={i} style={{ height: '21px', lineHeight: '21px' }}>
|
|
||||||
{i}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -151,6 +211,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
loadingContent,
|
loadingContent,
|
||||||
|
onChange,
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
onMentionInChat,
|
onMentionInChat,
|
||||||
@@ -158,8 +219,36 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
onShowRunner,
|
onShowRunner,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const lineCount = (content || '').split('\n').length;
|
const { wrapLines, cursorLine, cursorCol } = this.state;
|
||||||
const charCount = (content || '').length;
|
const text = content || '';
|
||||||
|
const lineCount = text.split('\n').length;
|
||||||
|
const charCount = text.length;
|
||||||
|
|
||||||
|
const { extension: langExt, label: langLabel } = getLanguageInfo(fileName, file?.mime);
|
||||||
|
|
||||||
|
// Build CodeMirror extensions list
|
||||||
|
const extensions = [
|
||||||
|
customEditorTheme,
|
||||||
|
scrollPastEnd(),
|
||||||
|
keymap.of([
|
||||||
|
indentWithTab,
|
||||||
|
{
|
||||||
|
key: 'Mod-s',
|
||||||
|
run: () => {
|
||||||
|
this.handleSaveShortcut();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (langExt) {
|
||||||
|
extensions.push(langExt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapLines) {
|
||||||
|
extensions.push(EditorView.lineWrapping);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -172,7 +261,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Editor Top Bar */}
|
{/* Editor Top Bar (Standard GUI Theme) */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 1.25,
|
p: 1.25,
|
||||||
@@ -189,7 +278,12 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
>
|
>
|
||||||
{/* File Name & Status */}
|
{/* File Name & Status */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0, mr: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0, mr: 1 }}>
|
||||||
<IconButton size="small" onClick={onClose} aria-label="Zurück zur Dateiliste" sx={{ p: 0.5, mr: 0.25 }}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Zurück zur Dateiliste"
|
||||||
|
sx={{ p: 0.5, mr: 0.25 }}
|
||||||
|
>
|
||||||
<ArrowBackRoundedIcon fontSize="small" />
|
<ArrowBackRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
|
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
|
||||||
@@ -232,16 +326,43 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
Exe
|
Exe
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Tooltip title={wrapLines ? 'Zeilenumbruch ausschalten' : 'Zeilenumbruch einschalten'}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleWrap}
|
||||||
|
color={wrapLines ? 'primary' : 'default'}
|
||||||
|
aria-label="Zeilenumbruch umschalten"
|
||||||
|
sx={{ p: 0.6 }}
|
||||||
|
>
|
||||||
|
<WrapTextRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{onMentionInChat && (
|
{onMentionInChat && (
|
||||||
<IconButton size="small" onClick={this.handleMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
<Tooltip title="Im Chat erwähnen (inkl. Selektion)">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleMentionInChat}
|
||||||
|
aria-label="In Chat erwähnen"
|
||||||
|
sx={{ p: 0.6 }}
|
||||||
|
>
|
||||||
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDirty && (
|
{isDirty && (
|
||||||
<IconButton size="small" onClick={this.handleRevert} aria-label="Änderungen verwerfen" sx={{ p: 0.6, color: 'text.secondary' }}>
|
<Tooltip title="Änderungen verwerfen">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleRevert}
|
||||||
|
aria-label="Änderungen verwerfen"
|
||||||
|
sx={{ p: 0.6, color: 'text.secondary' }}
|
||||||
|
>
|
||||||
<UndoRoundedIcon fontSize="small" />
|
<UndoRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -262,7 +383,12 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
{isSaving ? 'Speichern…' : 'Speichern (Strg+S)'}
|
{isSaving ? 'Speichern…' : 'Speichern (Strg+S)'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<IconButton size="small" onClick={onClose} aria-label="Editor schließen" sx={{ p: 0.6, ml: 0.5 }}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Editor schließen"
|
||||||
|
sx={{ p: 0.6, ml: 0.5 }}
|
||||||
|
>
|
||||||
<CloseRoundedIcon fontSize="small" />
|
<CloseRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -275,95 +401,87 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Editor Area */}
|
{/* Editor Area (Softer Dark Slate Code Background) */}
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
|
<Box sx={{ flexGrow: 1, minHeight: 0, position: 'relative', overflow: 'hidden', bgcolor: EDITOR_BG }}>
|
||||||
{loadingContent ? (
|
{loadingContent ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
color: 'text.secondary',
|
color: '#abb2bf',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={24} />
|
<CircularProgress size={24} />
|
||||||
<Typography variant="body2">Lade Dateiinhalt…</Typography>
|
<Typography variant="body2">Lade Dateiinhalt…</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
{/* Line Numbers Gutter (shown when wrap is off to maintain line alignment) */}
|
|
||||||
{!this.state.wrapLines && (
|
|
||||||
<Box
|
<Box
|
||||||
ref={this.gutterRef}
|
|
||||||
sx={(theme) => ({
|
|
||||||
width: 44,
|
|
||||||
flexShrink: 0,
|
|
||||||
bgcolor: theme.palette.mode === 'dark' ? '#0f1215' : '#f0f2f5',
|
|
||||||
color: 'text.disabled',
|
|
||||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
|
||||||
fontSize: '13px',
|
|
||||||
textAlign: 'right',
|
|
||||||
pr: 1.2,
|
|
||||||
pt: 1.5,
|
|
||||||
pb: 3,
|
|
||||||
userSelect: 'none',
|
|
||||||
overflow: 'hidden',
|
|
||||||
borderRight: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.renderLineNumbers(lineCount)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Textarea Code Input */}
|
|
||||||
<Box
|
|
||||||
component="textarea"
|
|
||||||
ref={this.textareaRef}
|
|
||||||
value={content || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
this.props.onChange(e.target.value);
|
|
||||||
this.handleCursorMove();
|
|
||||||
}}
|
|
||||||
onClick={this.handleCursorMove}
|
|
||||||
onKeyUp={this.handleCursorMove}
|
|
||||||
onSelect={this.handleCursorMove}
|
|
||||||
onScroll={this.handleScroll}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
wrap={this.state.wrapLines ? 'soft' : 'off'}
|
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
p: 1.5,
|
width: '100%',
|
||||||
border: 'none',
|
overflow: 'hidden',
|
||||||
outline: 'none',
|
'& .cm-theme': {
|
||||||
resize: 'none',
|
height: '100%',
|
||||||
bgcolor: 'transparent',
|
width: '100%',
|
||||||
color: 'text.primary',
|
},
|
||||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
'& .cm-editor': {
|
||||||
fontSize: '13px',
|
height: '100%',
|
||||||
lineHeight: '21px',
|
width: '100%',
|
||||||
tabSize: 2,
|
backgroundColor: EDITOR_BG,
|
||||||
whiteSpace: this.state.wrapLines ? 'pre-wrap' : 'pre',
|
},
|
||||||
wordBreak: this.state.wrapLines ? 'break-word' : 'normal',
|
'& .cm-scroller': {
|
||||||
overflowWrap: this.state.wrapLines ? 'anywhere' : 'normal',
|
height: '100% !important',
|
||||||
overflowX: this.state.wrapLines ? 'hidden' : 'auto',
|
overflow: 'auto !important',
|
||||||
overflowY: 'auto',
|
|
||||||
'&:focus': {
|
|
||||||
outline: 'none',
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<CodeMirror
|
||||||
|
value={text}
|
||||||
|
height="100%"
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
theme={oneDark}
|
||||||
|
extensions={extensions}
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
highlightActiveLineGutter: true,
|
||||||
|
highlightSpecialChars: true,
|
||||||
|
history: true,
|
||||||
|
foldGutter: true,
|
||||||
|
drawSelection: true,
|
||||||
|
dropCursor: true,
|
||||||
|
allowMultipleSelections: true,
|
||||||
|
indentOnInput: true,
|
||||||
|
syntaxHighlighting: true,
|
||||||
|
bracketMatching: true,
|
||||||
|
closeBrackets: true,
|
||||||
|
autocompletion: true,
|
||||||
|
rectangularSelection: true,
|
||||||
|
crosshairCursor: false,
|
||||||
|
highlightActiveLine: true,
|
||||||
|
highlightSelectionMatches: true,
|
||||||
|
closeBracketsKeymap: true,
|
||||||
|
defaultKeymap: true,
|
||||||
|
searchKeymap: true,
|
||||||
|
historyKeymap: true,
|
||||||
|
foldKeymap: true,
|
||||||
|
completionKeymap: true,
|
||||||
|
lintKeymap: true,
|
||||||
|
}}
|
||||||
|
onCreateEditor={this.handleEditorCreate}
|
||||||
|
onUpdate={this.handleViewUpdate}
|
||||||
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Status Bar */}
|
{/* Status Bar (Standard GUI Theme) */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: 2,
|
px: 2,
|
||||||
@@ -381,7 +499,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<span>
|
<span>
|
||||||
Zeile {this.state.cursorLine}, Spalte {this.state.cursorCol}
|
Zeile {cursorLine}, Spalte {cursorCol}
|
||||||
</span>
|
</span>
|
||||||
<span>{lineCount} Zeilen</span>
|
<span>{lineCount} Zeilen</span>
|
||||||
<span>{charCount} Zeichen</span>
|
<span>{charCount} Zeichen</span>
|
||||||
@@ -389,6 +507,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<span>{langLabel}</span>
|
||||||
<span>UTF-8</span>
|
<span>UTF-8</span>
|
||||||
<span>{file?.mime || 'text/plain'}</span>
|
<span>{file?.mime || 'text/plain'}</span>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user