2025-06-30 18:25:26 +00:00
|
|
|
|
import Prism from 'prismjs'
|
2025-07-01 06:10:32 +00:00
|
|
|
|
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
2025-06-30 18:25:26 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-01 06:10:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Форматирует HTML контент для лучшего отображения
|
|
|
|
|
* Убирает лишние пробелы и делает разметку красивой
|
|
|
|
|
*/
|
|
|
|
|
const formatHtmlContent = (html: string): string => {
|
|
|
|
|
if (!html || typeof html !== 'string') return ''
|
|
|
|
|
|
|
|
|
|
// Удаляем лишние пробелы между тегами
|
|
|
|
|
let formatted = html
|
|
|
|
|
.replace(/>\s+</g, '><') // Убираем пробелы между тегами
|
|
|
|
|
.replace(/\s+/g, ' ') // Множественные пробелы в одиночные
|
|
|
|
|
.trim() // Убираем пробелы в начале и конце
|
|
|
|
|
|
|
|
|
|
// Добавляем отступы для лучшего отображения
|
|
|
|
|
const indent = ' '
|
|
|
|
|
let indentLevel = 0
|
|
|
|
|
const lines: string[] = []
|
|
|
|
|
|
|
|
|
|
// Разбиваем на токены (теги и текст)
|
|
|
|
|
const tokens = formatted.match(/<[^>]+>|[^<]+/g) || []
|
|
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
|
if (token.startsWith('<')) {
|
|
|
|
|
if (token.startsWith('</')) {
|
|
|
|
|
// Закрывающий тег - уменьшаем отступ
|
|
|
|
|
indentLevel = Math.max(0, indentLevel - 1)
|
|
|
|
|
lines.push(indent.repeat(indentLevel) + token)
|
|
|
|
|
} else if (token.endsWith('/>')) {
|
|
|
|
|
// Самозакрывающийся тег
|
|
|
|
|
lines.push(indent.repeat(indentLevel) + token)
|
|
|
|
|
} else {
|
|
|
|
|
// Открывающий тег - добавляем отступ
|
|
|
|
|
lines.push(indent.repeat(indentLevel) + token)
|
|
|
|
|
indentLevel++
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Текстовое содержимое
|
|
|
|
|
const trimmed = token.trim()
|
|
|
|
|
if (trimmed) {
|
|
|
|
|
lines.push(indent.repeat(indentLevel) + trimmed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Генерирует номера строк для текста
|
|
|
|
|
*/
|
|
|
|
|
const generateLineNumbers = (text: string): string[] => {
|
|
|
|
|
if (!text) return ['1']
|
|
|
|
|
const lines = text.split('\n')
|
|
|
|
|
return lines.map((_, index) => String(index + 1))
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
/**
|
|
|
|
|
* Редактируемый компонент для кода с подсветкой синтаксиса
|
|
|
|
|
*/
|
|
|
|
|
const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|
|
|
|
const [isEditing, setIsEditing] = createSignal(false)
|
|
|
|
|
const [content, setContent] = createSignal(props.content)
|
|
|
|
|
let editorRef: HTMLDivElement | undefined
|
|
|
|
|
let highlightRef: HTMLPreElement | undefined
|
2025-07-01 06:10:32 +00:00
|
|
|
|
let lineNumbersRef: HTMLDivElement | undefined
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-01 06:10:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Обновляет номера строк
|
|
|
|
|
*/
|
|
|
|
|
const updateLineNumbers = () => {
|
|
|
|
|
if (!lineNumbersRef) return
|
|
|
|
|
|
|
|
|
|
const lineNumbers = generateLineNumbers(content())
|
|
|
|
|
lineNumbersRef.innerHTML = lineNumbers
|
|
|
|
|
.map(num => `<div class="${styles.lineNumber}">${num}</div>`)
|
|
|
|
|
.join('')
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
/**
|
|
|
|
|
* Синхронизирует скролл между редактором и подсветкой
|
|
|
|
|
*/
|
|
|
|
|
const syncScroll = () => {
|
|
|
|
|
if (editorRef && highlightRef) {
|
|
|
|
|
highlightRef.scrollTop = editorRef.scrollTop
|
|
|
|
|
highlightRef.scrollLeft = editorRef.scrollLeft
|
|
|
|
|
}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
if (editorRef && lineNumbersRef) {
|
|
|
|
|
lineNumbersRef.scrollTop = editorRef.scrollTop
|
|
|
|
|
}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обработчик изменения контента
|
|
|
|
|
*/
|
|
|
|
|
const handleInput = (e: Event) => {
|
|
|
|
|
const target = e.target as HTMLDivElement
|
2025-07-01 06:10:32 +00:00
|
|
|
|
|
|
|
|
|
// Сохраняем текущую позицию курсора
|
|
|
|
|
const selection = window.getSelection()
|
|
|
|
|
let caretOffset = 0
|
|
|
|
|
|
|
|
|
|
if (selection && selection.rangeCount > 0) {
|
|
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
const preCaretRange = range.cloneRange()
|
|
|
|
|
preCaretRange.selectNodeContents(target)
|
|
|
|
|
preCaretRange.setEnd(range.endContainer, range.endOffset)
|
|
|
|
|
caretOffset = preCaretRange.toString().length
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
const newContent = target.textContent || ''
|
|
|
|
|
setContent(newContent)
|
|
|
|
|
props.onContentChange(newContent)
|
|
|
|
|
updateHighlight()
|
2025-07-01 06:10:32 +00:00
|
|
|
|
updateLineNumbers()
|
|
|
|
|
|
|
|
|
|
// Восстанавливаем позицию курсора после обновления
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
if (target && selection) {
|
|
|
|
|
try {
|
|
|
|
|
const textNode = target.firstChild
|
|
|
|
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
|
|
|
const range = document.createRange()
|
|
|
|
|
const safeOffset = Math.min(caretOffset, textNode.textContent?.length || 0)
|
|
|
|
|
range.setStart(textNode, safeOffset)
|
|
|
|
|
range.setEnd(textNode, safeOffset)
|
|
|
|
|
selection.removeAllRanges()
|
|
|
|
|
selection.addRange(range)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Could not restore caret position:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обработчик сохранения
|
|
|
|
|
*/
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
if (props.onSave) {
|
|
|
|
|
props.onSave(content())
|
|
|
|
|
}
|
|
|
|
|
setIsEditing(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обработчик отмены
|
|
|
|
|
*/
|
|
|
|
|
const handleCancel = () => {
|
2025-07-01 06:10:32 +00:00
|
|
|
|
const originalContent = props.content
|
|
|
|
|
setContent(originalContent) // Возвращаем исходный контент
|
|
|
|
|
|
|
|
|
|
// Обновляем содержимое редактируемой области
|
|
|
|
|
if (editorRef) {
|
|
|
|
|
editorRef.textContent = originalContent
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
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 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()) {
|
2025-07-01 06:10:32 +00:00
|
|
|
|
const formattedContent = language() === 'markup' || language() === 'html'
|
|
|
|
|
? formatHtmlContent(props.content)
|
|
|
|
|
: props.content
|
|
|
|
|
setContent(formattedContent)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
updateHighlight()
|
2025-07-01 06:10:32 +00:00
|
|
|
|
updateLineNumbers()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Эффект для обновления подсветки при изменении контента
|
|
|
|
|
createEffect(() => {
|
|
|
|
|
content() // Реактивность
|
|
|
|
|
updateHighlight()
|
2025-07-01 06:10:32 +00:00
|
|
|
|
updateLineNumbers()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Эффект для синхронизации редактируемой области с content
|
|
|
|
|
createEffect(() => {
|
|
|
|
|
if (editorRef) {
|
|
|
|
|
const currentContent = content()
|
|
|
|
|
if (editorRef.textContent !== currentContent) {
|
|
|
|
|
// Сохраняем позицию курсора
|
|
|
|
|
const selection = window.getSelection()
|
|
|
|
|
let caretOffset = 0
|
|
|
|
|
|
|
|
|
|
if (selection && selection.rangeCount > 0 && isEditing()) {
|
|
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
const preCaretRange = range.cloneRange()
|
|
|
|
|
preCaretRange.selectNodeContents(editorRef)
|
|
|
|
|
preCaretRange.setEnd(range.endContainer, range.endOffset)
|
|
|
|
|
caretOffset = preCaretRange.toString().length
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
editorRef.textContent = currentContent
|
|
|
|
|
|
|
|
|
|
// Восстанавливаем курсор только в режиме редактирования
|
|
|
|
|
if (isEditing() && selection) {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
try {
|
|
|
|
|
const textNode = editorRef?.firstChild
|
|
|
|
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
|
|
|
const range = document.createRange()
|
|
|
|
|
const safeOffset = Math.min(caretOffset, textNode.textContent?.length || 0)
|
|
|
|
|
range.setStart(textNode, safeOffset)
|
|
|
|
|
range.setEnd(textNode, safeOffset)
|
|
|
|
|
selection.removeAllRanges()
|
|
|
|
|
selection.addRange(range)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Could not restore caret position:', error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
2025-07-01 06:10:32 +00:00
|
|
|
|
const formattedContent = language() === 'markup' || language() === 'html'
|
|
|
|
|
? formatHtmlContent(props.content)
|
|
|
|
|
: props.content
|
|
|
|
|
setContent(formattedContent)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
updateHighlight()
|
2025-07-01 06:10:32 +00:00
|
|
|
|
updateLineNumbers()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div class={styles.editableCodeContainer}>
|
2025-07-01 06:10:32 +00:00
|
|
|
|
{/* Контейнер редактора - увеличиваем размер */}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<div
|
|
|
|
|
class={styles.editorWrapper}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
style={`height: 100%; ${isEditing() ? 'border: 2px solid #007acc;' : ''}`}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
>
|
2025-07-01 06:10:32 +00:00
|
|
|
|
{/* Номера строк */}
|
|
|
|
|
<div
|
|
|
|
|
ref={lineNumbersRef}
|
|
|
|
|
class={styles.lineNumbersContainer}
|
|
|
|
|
style="position: absolute; left: 0; top: 0; width: 50px; height: 100%; background: #1e1e1e; border-right: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden; user-select: none; padding: 8px 0; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; line-height: 1.4;"
|
2025-06-30 18:25:26 +00:00
|
|
|
|
/>
|
|
|
|
|
|
2025-07-01 06:10:32 +00:00
|
|
|
|
{/* Подсветка синтаксиса (фон) - только в режиме редактирования */}
|
|
|
|
|
<Show when={isEditing()}>
|
|
|
|
|
<pre
|
|
|
|
|
ref={highlightRef}
|
|
|
|
|
class={`${styles.syntaxHighlight} language-${language()}`}
|
|
|
|
|
style="position: absolute; top: 0; left: 50px; right: 0; bottom: 0; pointer-events: none; color: transparent; background: transparent; margin: 0; padding: 8px 12px; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; overflow: hidden; z-index: 0;"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
|
|
|
|
</Show>
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
{/* Редактируемая область */}
|
|
|
|
|
<div
|
2025-07-01 06:10:32 +00:00
|
|
|
|
ref={(el) => {
|
|
|
|
|
editorRef = el
|
|
|
|
|
// Синхронизируем содержимое при создании элемента
|
|
|
|
|
if (el && el.textContent !== content()) {
|
|
|
|
|
el.textContent = content()
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
contentEditable={isEditing()}
|
|
|
|
|
class={styles.editorArea}
|
|
|
|
|
style={`
|
2025-07-01 06:10:32 +00:00
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 50px;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
2025-06-30 18:25:26 +00:00
|
|
|
|
z-index: 1;
|
2025-07-01 06:10:32 +00:00
|
|
|
|
background: ${isEditing() ? 'rgba(0, 0, 0, 0.02)' : 'transparent'};
|
2025-06-30 18:25:26 +00:00
|
|
|
|
color: ${isEditing() ? 'rgba(255, 255, 255, 0.9)' : 'transparent'};
|
|
|
|
|
margin: 0;
|
2025-07-01 06:10:32 +00:00
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.4;
|
2025-06-30 18:25:26 +00:00
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
outline: none;
|
|
|
|
|
cursor: ${isEditing() ? 'text' : 'default'};
|
|
|
|
|
caret-color: ${isEditing() ? '#fff' : 'transparent'};
|
|
|
|
|
`}
|
|
|
|
|
onInput={handleInput}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
onScroll={syncScroll}
|
|
|
|
|
spellcheck={false}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
/>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
|
|
|
|
{/* Превью для неактивного режима */}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
<Show when={!isEditing()}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<pre
|
|
|
|
|
class={`${styles.codePreview} language-${language()}`}
|
|
|
|
|
style={`
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
2025-07-01 06:10:32 +00:00
|
|
|
|
left: 50px;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
2025-06-30 18:25:26 +00:00
|
|
|
|
margin: 0;
|
2025-07-01 06:10:32 +00:00
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.4;
|
2025-06-30 18:25:26 +00:00
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
2025-07-01 06:10:32 +00:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
z-index: 2;
|
2025-06-30 18:25:26 +00:00
|
|
|
|
`}
|
|
|
|
|
onClick={() => setIsEditing(true)}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
onScroll={(e) => {
|
|
|
|
|
// Синхронизируем номера строк при скролле в режиме просмотра
|
|
|
|
|
if (lineNumbersRef) {
|
|
|
|
|
lineNumbersRef.scrollTop = (e.target as HTMLElement).scrollTop
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
>
|
|
|
|
|
<code
|
|
|
|
|
class={`language-${language()}`}
|
|
|
|
|
innerHTML={(() => {
|
|
|
|
|
try {
|
|
|
|
|
return Prism.highlight(content(), Prism.languages[language()], language())
|
|
|
|
|
} catch {
|
|
|
|
|
return content()
|
|
|
|
|
}
|
|
|
|
|
})()}
|
|
|
|
|
/>
|
|
|
|
|
</pre>
|
2025-07-01 06:10:32 +00:00
|
|
|
|
</Show>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-01 06:10:32 +00:00
|
|
|
|
{/* Индикатор языка */}
|
|
|
|
|
<span class={styles.languageBadge} style="top: 8px; right: 8px; z-index: 10;">
|
|
|
|
|
{language()}
|
|
|
|
|
</span>
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
{/* Плейсхолдер */}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
<Show when={!content()}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<div
|
|
|
|
|
class={styles.placeholder}
|
|
|
|
|
onClick={() => setIsEditing(true)}
|
2025-07-01 06:10:32 +00:00
|
|
|
|
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; cursor: pointer; font-style: italic; font-size: 14px;"
|
2025-06-30 18:25:26 +00:00
|
|
|
|
>
|
|
|
|
|
{props.placeholder || 'Нажмите для редактирования...'}
|
|
|
|
|
</div>
|
2025-07-01 06:10:32 +00:00
|
|
|
|
</Show>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-01 06:10:32 +00:00
|
|
|
|
{/* Кнопки управления внизу */}
|
|
|
|
|
{props.showButtons !== false && (
|
|
|
|
|
<div class={styles.editorControls} style="border-top: 1px solid rgba(255, 255, 255, 0.1); border-bottom: none; background-color: #1e1e1e;">
|
|
|
|
|
<Show when={isEditing()} fallback={
|
|
|
|
|
<button class={styles.editButton} onClick={() => setIsEditing(true)}>
|
|
|
|
|
✏️ Редактировать
|
|
|
|
|
</button>
|
|
|
|
|
}>
|
|
|
|
|
<div class={styles.editingControls}>
|
|
|
|
|
<button class={styles.saveButton} onClick={handleSave}>
|
|
|
|
|
💾 Сохранить (Ctrl+Enter)
|
|
|
|
|
</button>
|
|
|
|
|
<button class={styles.cancelButton} onClick={handleCancel}>
|
|
|
|
|
❌ Отмена (Esc)
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default EditableCodePreview
|