2024-09-15 18:47:21 +00:00
|
|
|
import CharacterCount from '@tiptap/extension-character-count'
|
|
|
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
|
|
import clsx from 'clsx'
|
2024-09-27 16:31:54 +00:00
|
|
|
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
2024-10-01 17:19:40 +00:00
|
|
|
import { createTiptapEditor, useEditorHTML, useEditorIsEmpty } from 'solid-tiptap'
|
2024-09-27 18:09:50 +00:00
|
|
|
import { Button } from '~/components/_shared/Button'
|
|
|
|
import { useLocalize } from '~/context/localize'
|
2024-09-24 06:48:39 +00:00
|
|
|
import { base } from '~/lib/editorExtensions'
|
2024-10-01 19:39:17 +00:00
|
|
|
import { ToolbarControl as Control } from './Toolbar/ToolbarControl'
|
2024-09-15 18:47:21 +00:00
|
|
|
|
2024-10-01 17:18:27 +00:00
|
|
|
import { Editor } from '@tiptap/core'
|
|
|
|
import { Portal } from 'solid-js/web'
|
2024-10-01 17:19:40 +00:00
|
|
|
import { UploadModalContent } from '~/components/Upload/UploadModalContent'
|
2024-10-01 17:18:27 +00:00
|
|
|
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
2024-10-01 17:19:40 +00:00
|
|
|
import { Icon } from '~/components/_shared/Icon/Icon'
|
|
|
|
import { Modal } from '~/components/_shared/Modal'
|
|
|
|
import { useUI } from '~/context/ui'
|
2024-10-01 17:18:27 +00:00
|
|
|
import { UploadedFile } from '~/types/upload'
|
2024-10-01 17:19:40 +00:00
|
|
|
import styles from './MiniEditor.module.scss'
|
2024-10-01 19:39:17 +00:00
|
|
|
import { InsertLinkForm } from './Toolbar/InsertLinkForm'
|
2024-09-15 18:47:21 +00:00
|
|
|
|
|
|
|
interface MiniEditorProps {
|
|
|
|
content?: string
|
|
|
|
onChange?: (content: string) => void
|
2024-09-27 16:31:54 +00:00
|
|
|
onSubmit?: (content: string) => void
|
|
|
|
onCancel?: () => void
|
2024-09-15 18:47:21 +00:00
|
|
|
limit?: number
|
|
|
|
placeholder?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
2024-09-27 17:57:25 +00:00
|
|
|
const { t } = useLocalize()
|
2024-10-01 17:18:27 +00:00
|
|
|
const { showModal } = useUI()
|
2024-09-15 18:47:21 +00:00
|
|
|
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
|
|
|
const [counter, setCounter] = createSignal(0)
|
2024-10-01 17:18:27 +00:00
|
|
|
const [showLinkForm, setShowLinkForm] = createSignal(false)
|
2024-09-15 18:47:21 +00:00
|
|
|
const editor = createTiptapEditor(() => ({
|
|
|
|
element: editorElement()!,
|
|
|
|
extensions: [
|
|
|
|
...base,
|
|
|
|
Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }),
|
|
|
|
CharacterCount.configure({ limit: props.limit })
|
|
|
|
],
|
|
|
|
editorProps: {
|
|
|
|
attributes: {
|
2024-10-01 19:52:45 +00:00
|
|
|
class: styles.compactEditor
|
2024-09-15 18:47:21 +00:00
|
|
|
}
|
|
|
|
},
|
2024-10-01 17:18:27 +00:00
|
|
|
content: props.content || '',
|
|
|
|
autofocus: 'end'
|
2024-09-15 18:47:21 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
const html = useEditorHTML(editor)
|
2024-10-01 17:18:27 +00:00
|
|
|
const isEmpty = useEditorIsEmpty(editor)
|
|
|
|
|
|
|
|
const toggleLinkForm = () => {
|
|
|
|
setShowLinkForm(!showLinkForm())
|
|
|
|
// Если форма закрывается, возвращаем фокус редактору
|
|
|
|
!showLinkForm() && editor()?.commands.focus()
|
|
|
|
}
|
|
|
|
|
|
|
|
const setLink = (url: string) => {
|
|
|
|
url && editor()?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
|
|
|
!url && editor()?.chain().focus().extendMarkRange('link').unsetLink().run()
|
|
|
|
setShowLinkForm(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeLink = () => {
|
|
|
|
editor()?.chain().focus().unsetLink().run()
|
|
|
|
setShowLinkForm(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleLinkButtonClick = () => {
|
|
|
|
if (editor()?.isActive('link')) {
|
|
|
|
const previousUrl = editor()?.getAttributes('link').href
|
|
|
|
const url = window.prompt('URL', previousUrl)
|
|
|
|
url && setLink(url)
|
|
|
|
} else {
|
|
|
|
toggleLinkForm()
|
|
|
|
}
|
|
|
|
}
|
2024-09-27 17:57:25 +00:00
|
|
|
|
2024-09-27 14:26:40 +00:00
|
|
|
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
2024-09-15 18:47:21 +00:00
|
|
|
|
|
|
|
createEffect(() => {
|
|
|
|
const textLength = editor()?.getText().length || 0
|
|
|
|
setCounter(textLength)
|
|
|
|
const content = html()
|
|
|
|
content && props.onChange?.(content)
|
|
|
|
})
|
|
|
|
|
2024-09-27 17:57:25 +00:00
|
|
|
const handleSubmit = () => {
|
|
|
|
html() && props.onSubmit?.(html() || '')
|
|
|
|
editor()?.commands.clearContent(true)
|
|
|
|
}
|
2024-09-15 18:47:21 +00:00
|
|
|
|
|
|
|
return (
|
2024-10-01 17:18:27 +00:00
|
|
|
<div class={clsx(styles.MiniEditor, styles.isFocused)}>
|
|
|
|
<div class={clsx(styles.controls, styles.isFocused)}>
|
|
|
|
<div class={clsx(styles.actions, styles.active)}>
|
|
|
|
<Control
|
|
|
|
key="bold"
|
|
|
|
editor={editor()}
|
|
|
|
onChange={() => editor()?.chain().focus().toggleBold().run()}
|
|
|
|
title={t('Bold')}
|
|
|
|
>
|
|
|
|
<Icon name="editor-bold" />
|
|
|
|
</Control>
|
|
|
|
<Control
|
|
|
|
key="italic"
|
|
|
|
editor={editor()}
|
|
|
|
onChange={() => editor()?.chain().focus().toggleItalic().run()}
|
|
|
|
title={t('Italic')}
|
|
|
|
>
|
|
|
|
<Icon name="editor-italic" />
|
|
|
|
</Control>
|
|
|
|
<Control
|
|
|
|
key="link"
|
|
|
|
editor={editor()}
|
|
|
|
onChange={handleLinkButtonClick}
|
|
|
|
title={t('Add url')}
|
|
|
|
isActive={(e: Editor) => Boolean(e?.isActive('link'))}
|
|
|
|
>
|
|
|
|
<Icon name="editor-link" />
|
|
|
|
</Control>
|
|
|
|
<Control
|
|
|
|
key="blockquote"
|
|
|
|
editor={editor()}
|
|
|
|
onChange={() => editor()?.chain().focus().toggleBlockquote().run()}
|
|
|
|
title={t('Add blockquote')}
|
|
|
|
>
|
|
|
|
<Icon name="editor-quote" />
|
|
|
|
</Control>
|
|
|
|
<Control
|
|
|
|
key="image"
|
|
|
|
editor={editor()}
|
|
|
|
onChange={() => showModal('simplifiedEditorUploadImage')}
|
|
|
|
title={t('Add image')}
|
|
|
|
>
|
|
|
|
<Icon name="editor-image-dd-full" />
|
|
|
|
</Control>
|
|
|
|
<InsertLinkForm
|
|
|
|
class={clsx([styles.linkInput, { [styles.linkInputactive]: showLinkForm() }])}
|
|
|
|
editor={editor() as Editor}
|
|
|
|
onClose={toggleLinkForm}
|
|
|
|
onSubmit={setLink}
|
|
|
|
onRemove={removeLink}
|
2024-09-27 17:57:25 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2024-10-01 17:18:27 +00:00
|
|
|
</div>
|
2024-09-27 14:26:40 +00:00
|
|
|
|
2024-10-01 17:18:27 +00:00
|
|
|
<Portal>
|
|
|
|
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
|
|
|
<UploadModalContent
|
|
|
|
onClose={(image) => renderUploadedImage(editor() as Editor, image as UploadedFile)}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
</Portal>
|
|
|
|
|
|
|
|
<div id="mini-editor" ref={setEditorElement} style={styles.minimal} />
|
|
|
|
|
|
|
|
<div class={styles.buttons}>
|
|
|
|
<Button
|
|
|
|
value={t('Cancel')}
|
|
|
|
disabled={isEmpty()}
|
|
|
|
variant="secondary"
|
|
|
|
onClick={() => editor()?.commands.clearContent()}
|
|
|
|
/>
|
|
|
|
<Button value={t('Send')} variant="primary" disabled={isEmpty()} onClick={handleSubmit} />
|
2024-09-15 18:47:21 +00:00
|
|
|
</div>
|
2024-10-01 17:18:27 +00:00
|
|
|
|
|
|
|
<Show when={counter() > 0}>
|
|
|
|
<small class={styles.limit}>
|
|
|
|
{counter()} / {props.limit || '∞'}
|
|
|
|
</small>
|
|
|
|
</Show>
|
2024-09-15 18:47:21 +00:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|