core/panel/ui/EditableCodePreview.tsx

384 lines
13 KiB
TypeScript
Raw Normal View History

2025-07-02 19:30:21 +00:00
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
2025-06-30 18:25:26 +00:00
import 'prismjs/themes/prism-tomorrow.css'
import styles from '../styles/CodePreview.module.css'
2025-07-02 19:30:21 +00:00
import {
DEFAULT_EDITOR_CONFIG,
detectLanguage,
formatCode,
handleTabKey,
highlightCode
} from '../utils/codeHelpers'
2025-06-30 18:25:26 +00:00
interface EditableCodePreviewProps {
content: string
onContentChange: (newContent: string) => void
onSave?: (content: string) => void
onCancel?: () => void
language?: string
maxHeight?: string
placeholder?: string
showButtons?: boolean
2025-07-02 19:30:21 +00:00
autoFormat?: boolean
readOnly?: boolean
theme?: 'dark' | 'light' | 'highContrast'
2025-06-30 18:25:26 +00:00
}
2025-07-01 06:10:32 +00:00
/**
2025-07-02 19:30:21 +00:00
* Современный редактор кода с подсветкой синтаксиса и удобными возможностями редактирования
*
* Возможности:
* - Подсветка синтаксиса в реальном времени
* - Номера строк с синхронизацией скролла
* - Автоформатирование кода
* - Горячие клавиши (Ctrl+Enter для сохранения, Esc для отмены)
* - Обработка Tab для отступов
* - Сохранение позиции курсора
* - Адаптивный дизайн
* - Поддержка тем оформления
2025-06-30 18:25:26 +00:00
*/
const EditableCodePreview = (props: EditableCodePreviewProps) => {
2025-07-02 19:30:21 +00:00
// Состояние компонента
2025-06-30 18:25:26 +00:00
const [isEditing, setIsEditing] = createSignal(false)
const [content, setContent] = createSignal(props.content)
2025-07-02 19:30:21 +00:00
const [isSaving, setIsSaving] = createSignal(false)
const [hasChanges, setHasChanges] = createSignal(false)
// Ссылки на DOM элементы
let editorRef: HTMLTextAreaElement | undefined
2025-06-30 18:25:26 +00:00
let highlightRef: HTMLPreElement | undefined
2025-07-01 06:10:32 +00:00
let lineNumbersRef: HTMLDivElement | undefined
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Реактивные вычисления
const language = createMemo(() => props.language || detectLanguage(content()))
// Контент для отображения (отформатированный в режиме просмотра, исходный в режиме редактирования)
const displayContent = createMemo(() => {
if (isEditing()) {
return content() // В режиме редактирования показываем исходный код
}
return props.autoFormat ? formatCode(content(), language()) : content() // В режиме просмотра - форматированный
})
const isEmpty = createMemo(() => !content()?.trim())
const status = createMemo(() => {
if (isSaving()) return 'saving'
if (isEditing()) return 'editing'
return 'idle'
})
2025-06-30 18:25:26 +00:00
/**
2025-07-02 19:30:21 +00:00
* Синхронизирует скролл подсветки синтаксиса с textarea
2025-06-30 18:25:26 +00:00
*/
2025-07-02 19:30:21 +00:00
const syncScroll = () => {
if (!editorRef) return
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
const scrollTop = editorRef.scrollTop
const scrollLeft = editorRef.scrollLeft
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Синхронизируем только подсветку синтаксиса в режиме редактирования
if (highlightRef && isEditing()) {
highlightRef.scrollTop = scrollTop
highlightRef.scrollLeft = scrollLeft
2025-06-30 18:25:26 +00:00
}
}
2025-07-01 06:10:32 +00:00
/**
2025-07-02 19:30:21 +00:00
* Генерирует элементы номеров строк для CSS счетчика
*/
const generateLineElements = createMemo(() => {
const lines = displayContent().split('\n')
return lines.map((_, _index) => <div class={styles.lineNumberItem} />)
})
/**
* Обработчик изменения контента
2025-07-01 06:10:32 +00:00
*/
2025-07-02 19:30:21 +00:00
const handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement
const newContent = target.value
2025-07-01 06:10:32 +00:00
2025-07-02 19:30:21 +00:00
setContent(newContent)
setHasChanges(newContent !== props.content)
props.onContentChange(newContent)
2025-07-01 06:10:32 +00:00
}
2025-06-30 18:25:26 +00:00
/**
2025-07-02 19:30:21 +00:00
* Обработчик горячих клавиш
2025-06-30 18:25:26 +00:00
*/
2025-07-02 19:30:21 +00:00
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+Enter или Cmd+Enter для сохранения
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault()
void handleSave()
return
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
// Escape для отмены
if (e.key === 'Escape') {
e.preventDefault()
handleCancel()
return
}
// Ctrl+Shift+F для форматирования
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'f') {
e.preventDefault()
handleFormat()
return
}
// Tab для отступов
if (handleTabKey(e)) {
// Обновляем контент после вставки отступа
setTimeout(() => {
const _target = e.target as HTMLTextAreaElement
handleInput(e)
}, 0)
2025-07-01 06:10:32 +00:00
}
2025-06-30 18:25:26 +00:00
}
/**
2025-07-02 19:30:21 +00:00
* Форматирование кода
2025-06-30 18:25:26 +00:00
*/
2025-07-02 19:30:21 +00:00
const handleFormat = () => {
if (!props.autoFormat) return
const formatted = formatCode(content(), language())
if (formatted !== content()) {
setContent(formatted)
setHasChanges(true)
props.onContentChange(formatted)
// Обновляем textarea
if (editorRef) {
editorRef.value = formatted
2025-07-01 06:10:32 +00:00
}
2025-07-02 19:30:21 +00:00
}
2025-06-30 18:25:26 +00:00
}
/**
2025-07-02 19:30:21 +00:00
* Сохранение изменений
2025-06-30 18:25:26 +00:00
*/
2025-07-02 19:30:21 +00:00
const handleSave = async () => {
if (!props.onSave || isSaving()) return
setIsSaving(true)
try {
await props.onSave(content())
setHasChanges(false)
setIsEditing(false)
} catch (error) {
console.error('Ошибка при сохранении:', error)
} finally {
setIsSaving(false)
2025-06-30 18:25:26 +00:00
}
}
/**
2025-07-02 19:30:21 +00:00
* Отмена изменений
2025-06-30 18:25:26 +00:00
*/
const handleCancel = () => {
2025-07-01 06:10:32 +00:00
const originalContent = props.content
2025-07-02 19:30:21 +00:00
setContent(originalContent)
setHasChanges(false)
2025-07-01 06:10:32 +00:00
2025-07-02 19:30:21 +00:00
// Обновляем textarea
2025-07-01 06:10:32 +00:00
if (editorRef) {
2025-07-02 19:30:21 +00:00
editorRef.value = originalContent
2025-07-01 06:10:32 +00:00
}
2025-06-30 18:25:26 +00:00
if (props.onCancel) {
props.onCancel()
}
setIsEditing(false)
}
/**
2025-07-02 19:30:21 +00:00
* Переход в режим редактирования
2025-06-30 18:25:26 +00:00
*/
2025-07-02 19:30:21 +00:00
const startEditing = () => {
if (props.readOnly) return
// Форматируем контент при переходе в режим редактирования, если автоформатирование включено
if (props.autoFormat) {
const formatted = formatCode(content(), language())
if (formatted !== content()) {
setContent(formatted)
props.onContentChange(formatted)
}
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
setIsEditing(true)
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Фокус на editor после рендера
setTimeout(() => {
if (editorRef) {
editorRef.focus()
// Устанавливаем курсор в конец
editorRef.setSelectionRange(editorRef.value.length, editorRef.value.length)
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
}, 50)
2025-06-30 18:25:26 +00:00
}
// Эффект для обновления контента при изменении props
createEffect(() => {
if (!isEditing()) {
2025-07-02 19:30:21 +00:00
setContent(props.content)
setHasChanges(false)
2025-06-30 18:25:26 +00:00
}
})
2025-07-02 19:30:21 +00:00
// Эффект для синхронизации textarea с content
2025-07-01 06:10:32 +00:00
createEffect(() => {
2025-07-02 19:30:21 +00:00
if (editorRef && editorRef.value !== content()) {
editorRef.value = content()
2025-07-01 06:10:32 +00:00
}
2025-06-30 18:25:26 +00:00
})
return (
2025-07-02 19:30:21 +00:00
<div class={`${styles.editableCodeContainer} ${styles[props.theme || 'darkTheme']}`}>
{/* Основной контейнер редактора */}
<div class={`${styles.editorContainer} ${isEditing() ? styles.editing : ''}`}>
{/* Область кода */}
<div class={styles.codeArea}>
{/* Контейнер для кода со скроллом */}
<div class={styles.codeContentWrapper}>
{/* Контейнер для самого кода */}
<div class={styles.codeTextWrapper}>
{/* Нумерация строк внутри скроллящегося контейнера */}
<div ref={lineNumbersRef} class={styles.lineNumbers}>
{generateLineElements()}
</div>
{/* Подсветка синтаксиса в режиме редактирования */}
<Show when={isEditing()}>
<pre
ref={highlightRef}
class={styles.syntaxHighlight}
aria-hidden="true"
innerHTML={highlightCode(displayContent(), language())}
/>
</Show>
{/* Режим просмотра или редактирования */}
<Show
when={isEditing()}
fallback={
<Show
when={!isEmpty()}
fallback={
<div class={styles.placeholderClickable} onClick={startEditing}>
{props.placeholder || 'Нажмите для редактирования...'}
</div>
}
>
<pre
class={styles.codePreviewContent}
onClick={startEditing}
innerHTML={highlightCode(displayContent(), language())}
/>
</Show>
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
>
<textarea
ref={editorRef}
class={styles.editorTextarea}
value={content()}
onInput={handleInput}
onKeyDown={handleKeyDown}
onScroll={syncScroll}
placeholder={props.placeholder || 'Введите код...'}
spellcheck={false}
autocomplete="off"
autocorrect="off"
autocapitalize="off"
wrap="off"
style={`
font-family: ${DEFAULT_EDITOR_CONFIG.fontFamily};
font-size: ${DEFAULT_EDITOR_CONFIG.fontSize}px;
line-height: ${DEFAULT_EDITOR_CONFIG.lineHeight};
tab-size: ${DEFAULT_EDITOR_CONFIG.tabSize};
background: transparent;
color: transparent;
caret-color: var(--code-text);
`}
/>
</Show>
</div>
</div>
</div>
2025-06-30 18:25:26 +00:00
</div>
2025-07-02 19:30:21 +00:00
{/* Панель управления */}
<div class={styles.controls}>
{/* Левая часть - информация */}
<div class={styles.controlsLeft}>
<span class={styles.languageBadge}>{language()}</span>
<div class={styles.statusIndicator}>
<div class={`${styles.statusDot} ${styles[status()]}`} />
<span>
{status() === 'saving' && 'Сохранение...'}
{status() === 'editing' && 'Редактирование'}
{status() === 'idle' && (hasChanges() ? 'Есть изменения' : 'Сохранено')}
</span>
</div>
<Show when={hasChanges()}>
<span style="color: var(--code-warning); font-size: 11px;"></span>
2025-07-01 06:10:32 +00:00
</Show>
</div>
2025-07-02 19:30:21 +00:00
{/* Правая часть - кнопки */}
<Show when={props.showButtons !== false}>
<div class={styles.controlsRight}>
<Show
when={!isEditing()}
fallback={
<div class={`${styles.editingControls} ${styles.fadeIn}`}>
<Show when={props.autoFormat}>
<button
class={styles.formatButton}
onClick={handleFormat}
disabled={isSaving()}
title="Форматировать код (Ctrl+Shift+F)"
>
🎨 Форматировать
</button>
</Show>
<button
class={styles.saveButton}
onClick={handleSave}
disabled={isSaving() || !hasChanges()}
title="Сохранить изменения (Ctrl+Enter)"
>
{isSaving() ? '⏳ Сохранение...' : '💾 Сохранить'}
</button>
<button
class={styles.cancelButton}
onClick={handleCancel}
disabled={isSaving()}
title="Отменить изменения (Esc)"
>
Отмена
</button>
</div>
}
>
<Show when={!props.readOnly}>
<button class={styles.editButton} onClick={startEditing} title="Редактировать код">
Редактировать
</button>
</Show>
</Show>
</div>
</Show>
</div>
2025-06-30 18:25:26 +00:00
</div>
)
}
export default EditableCodePreview