import Prism from 'prismjs' import { createEffect, createSignal, onMount } from 'solid-js' import 'prismjs/components/prism-json' import 'prismjs/components/prism-markup' import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-css' import 'prismjs/themes/prism-tomorrow.css' import styles from '../styles/CodePreview.module.css' import { detectLanguage } from './CodePreview' interface EditableCodePreviewProps { content: string onContentChange: (newContent: string) => void onSave?: (content: string) => void onCancel?: () => void language?: string maxHeight?: string placeholder?: string showButtons?: boolean } /** * Редактируемый компонент для кода с подсветкой синтаксиса */ const EditableCodePreview = (props: EditableCodePreviewProps) => { const [isEditing, setIsEditing] = createSignal(false) const [content, setContent] = createSignal(props.content) let editorRef: HTMLDivElement | undefined let highlightRef: HTMLPreElement | undefined const language = () => props.language || detectLanguage(content()) /** * Обновляет подсветку синтаксиса */ const updateHighlight = () => { if (!highlightRef) return const code = content() || '' const lang = language() try { if (Prism.languages[lang]) { highlightRef.innerHTML = Prism.highlight(code, Prism.languages[lang], lang) } else { highlightRef.textContent = code } } catch (e) { console.error('Error highlighting code:', e) highlightRef.textContent = code } } /** * Синхронизирует скролл между редактором и подсветкой */ const syncScroll = () => { if (editorRef && highlightRef) { highlightRef.scrollTop = editorRef.scrollTop highlightRef.scrollLeft = editorRef.scrollLeft } } /** * Обработчик изменения контента */ const handleInput = (e: Event) => { const target = e.target as HTMLDivElement const newContent = target.textContent || '' setContent(newContent) props.onContentChange(newContent) updateHighlight() } /** * Обработчик сохранения */ const handleSave = () => { if (props.onSave) { props.onSave(content()) } setIsEditing(false) } /** * Обработчик отмены */ const handleCancel = () => { setContent(props.content) // Возвращаем исходный контент if (props.onCancel) { props.onCancel() } setIsEditing(false) } /** * Обработчик клавиш */ const handleKeyDown = (e: KeyboardEvent) => { // Ctrl+Enter или Cmd+Enter для сохранения if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault() handleSave() return } // Escape для отмены if (e.key === 'Escape') { e.preventDefault() handleCancel() return } // Tab для отступа if (e.key === 'Tab') { e.preventDefault() // const target = e.target as HTMLDivElement const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) range.deleteContents() range.insertNode(document.createTextNode(' ')) // Два пробела range.collapse(false) selection.removeAllRanges() selection.addRange(range) handleInput(e) } } } // Эффект для обновления контента при изменении props createEffect(() => { if (!isEditing()) { setContent(props.content) updateHighlight() } }) // Эффект для обновления подсветки при изменении контента createEffect(() => { content() // Реактивность updateHighlight() }) onMount(() => { updateHighlight() }) return (
{/* Кнопки управления */} {props.showButtons !== false && (
{!isEditing() ? ( ) : (
)}
)} {/* Контейнер редактора */}
{/* Подсветка синтаксиса (фон) */}
{/* Плейсхолдер */} {!content() && (
setIsEditing(true)} style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; cursor: pointer; font-style: italic;" > {props.placeholder || 'Нажмите для редактирования...'}
)} {/* Индикатор языка */} {language()}
) } export default EditableCodePreview