import { createEffect, createMemo, createSignal, Show } from 'solid-js' import 'prismjs/themes/prism-tomorrow.css' import styles from '../styles/CodePreview.module.css' import { DEFAULT_EDITOR_CONFIG, detectLanguage, formatCode, handleTabKey, highlightCode } from '../utils/codeHelpers' interface EditableCodePreviewProps { content: string onContentChange: (newContent: string) => void onSave?: (content: string) => void onCancel?: () => void language?: string maxHeight?: string placeholder?: string showButtons?: boolean autoFormat?: boolean readOnly?: boolean theme?: 'dark' | 'light' | 'highContrast' } /** * Современный редактор кода с подсветкой синтаксиса и удобными возможностями редактирования * * Возможности: * - Подсветка синтаксиса в реальном времени * - Номера строк с синхронизацией скролла * - Автоформатирование кода * - Горячие клавиши (Ctrl+Enter для сохранения, Esc для отмены) * - Обработка Tab для отступов * - Сохранение позиции курсора * - Адаптивный дизайн * - Поддержка тем оформления */ const EditableCodePreview = (props: EditableCodePreviewProps) => { // Состояние компонента const [isEditing, setIsEditing] = createSignal(false) const [content, setContent] = createSignal(props.content) const [isSaving, setIsSaving] = createSignal(false) const [hasChanges, setHasChanges] = createSignal(false) // Ссылки на DOM элементы let editorRef: HTMLTextAreaElement | undefined let highlightRef: HTMLPreElement | undefined let lineNumbersRef: HTMLDivElement | undefined // Реактивные вычисления 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' }) /** * Синхронизирует скролл подсветки синтаксиса с textarea */ const syncScroll = () => { if (!editorRef) return const scrollTop = editorRef.scrollTop const scrollLeft = editorRef.scrollLeft // Синхронизируем только подсветку синтаксиса в режиме редактирования if (highlightRef && isEditing()) { highlightRef.scrollTop = scrollTop highlightRef.scrollLeft = scrollLeft } } /** * Генерирует элементы номеров строк для CSS счетчика */ const generateLineElements = createMemo(() => { const lines = displayContent().split('\n') return lines.map((_, _index) =>
) }) /** * Обработчик изменения контента */ const handleInput = (e: Event) => { const target = e.target as HTMLTextAreaElement const newContent = target.value setContent(newContent) setHasChanges(newContent !== props.content) props.onContentChange(newContent) } /** * Обработчик горячих клавиш */ const handleKeyDown = (e: KeyboardEvent) => { // Ctrl+Enter или Cmd+Enter для сохранения if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault() void handleSave() return } // 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) } } /** * Форматирование кода */ 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 } } } /** * Сохранение изменений */ 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) } } /** * Отмена изменений */ const handleCancel = () => { const originalContent = props.content setContent(originalContent) setHasChanges(false) // Обновляем textarea if (editorRef) { editorRef.value = originalContent } if (props.onCancel) { props.onCancel() } setIsEditing(false) } /** * Переход в режим редактирования */ const startEditing = () => { if (props.readOnly) return // Форматируем контент при переходе в режим редактирования, если автоформатирование включено if (props.autoFormat) { const formatted = formatCode(content(), language()) if (formatted !== content()) { setContent(formatted) props.onContentChange(formatted) } } setIsEditing(true) // Фокус на editor после рендера setTimeout(() => { if (editorRef) { editorRef.focus() // Устанавливаем курсор в конец editorRef.setSelectionRange(editorRef.value.length, editorRef.value.length) } }, 50) } // Эффект для обновления контента при изменении props createEffect(() => { if (!isEditing()) { setContent(props.content) setHasChanges(false) } }) // Эффект для синхронизации textarea с content createEffect(() => { if (editorRef && editorRef.value !== content()) { editorRef.value = content() } }) return (