webapp/src/components/Editor/MiniEditor.tsx

180 lines
5.8 KiB
TypeScript
Raw Normal View History

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>
)
}