2023-07-24 08:58:07 +00:00
|
|
|
import { createEffect, onCleanup, onMount, Show } from 'solid-js'
|
|
|
|
import {
|
|
|
|
createEditorTransaction,
|
|
|
|
createTiptapEditor,
|
|
|
|
useEditorHTML,
|
|
|
|
useEditorIsEmpty,
|
|
|
|
useEditorIsFocused
|
|
|
|
} from 'solid-tiptap'
|
|
|
|
import { useEditorContext } from '../../context/editor'
|
|
|
|
import { Document } from '@tiptap/extension-document'
|
|
|
|
import { Text } from '@tiptap/extension-text'
|
|
|
|
import { Paragraph } from '@tiptap/extension-paragraph'
|
|
|
|
import { Bold } from '@tiptap/extension-bold'
|
|
|
|
import { Button } from '../_shared/Button'
|
|
|
|
import { useLocalize } from '../../context/localize'
|
|
|
|
import { Icon } from '../_shared/Icon'
|
|
|
|
import { Popover } from '../_shared/Popover'
|
|
|
|
import { Italic } from '@tiptap/extension-italic'
|
|
|
|
import { Modal } from '../Nav/Modal'
|
|
|
|
import { hideModal, showModal } from '../../stores/ui'
|
|
|
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
|
|
|
import { UploadModalContent } from './UploadModalContent'
|
|
|
|
import { imageProxy } from '../../utils/imageProxy'
|
|
|
|
import { clsx } from 'clsx'
|
|
|
|
import styles from './SimplifiedEditor.module.scss'
|
|
|
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
|
|
|
import { InsertLinkForm } from './InsertLinkForm'
|
2023-07-27 17:38:38 +00:00
|
|
|
import { Link } from '@tiptap/extension-link'
|
2023-08-15 09:38:49 +00:00
|
|
|
import { UploadedFile } from '../../pages/types'
|
|
|
|
import { Figure } from './extensions/Figure'
|
2023-08-17 10:36:27 +00:00
|
|
|
import { Image } from '@tiptap/extension-image'
|
|
|
|
import { Figcaption } from './extensions/Figcaption'
|
2023-07-24 08:58:07 +00:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
initialContent?: string
|
2023-08-13 21:26:40 +00:00
|
|
|
onSubmit?: (text: string) => void
|
2023-08-17 10:36:27 +00:00
|
|
|
onChange?: (text: string) => void
|
2023-07-24 08:58:07 +00:00
|
|
|
placeholder: string
|
2023-07-27 17:38:38 +00:00
|
|
|
submitButtonText?: string
|
2023-07-24 08:58:07 +00:00
|
|
|
quoteEnabled?: boolean
|
|
|
|
imageEnabled?: boolean
|
|
|
|
setClear?: boolean
|
|
|
|
smallHeight?: boolean
|
|
|
|
submitByEnter?: boolean
|
|
|
|
submitByShiftEnter?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
const SimplifiedEditor = (props: Props) => {
|
|
|
|
const { t } = useLocalize()
|
|
|
|
|
2023-08-17 10:36:27 +00:00
|
|
|
const wrapperEditorElRef: {
|
|
|
|
current: HTMLElement
|
|
|
|
} = {
|
|
|
|
current: null
|
|
|
|
}
|
|
|
|
|
2023-07-24 08:58:07 +00:00
|
|
|
const editorElRef: {
|
2023-08-17 10:36:27 +00:00
|
|
|
current: HTMLElement
|
2023-07-24 08:58:07 +00:00
|
|
|
} = {
|
|
|
|
current: null
|
|
|
|
}
|
2023-08-17 10:36:27 +00:00
|
|
|
|
2023-07-24 08:58:07 +00:00
|
|
|
const {
|
|
|
|
actions: { setEditor }
|
|
|
|
} = useEditorContext()
|
|
|
|
|
2023-08-15 09:38:49 +00:00
|
|
|
const ImageFigure = Figure.extend({
|
|
|
|
name: 'capturedImage',
|
|
|
|
content: 'figcaption image'
|
|
|
|
})
|
|
|
|
|
2023-07-24 08:58:07 +00:00
|
|
|
const editor = createTiptapEditor(() => ({
|
|
|
|
element: editorElRef.current,
|
2023-07-24 14:09:04 +00:00
|
|
|
editorProps: {
|
|
|
|
attributes: {
|
|
|
|
class: styles.simplifiedEditorField
|
|
|
|
}
|
|
|
|
},
|
2023-07-24 08:58:07 +00:00
|
|
|
extensions: [
|
|
|
|
Document,
|
|
|
|
Text,
|
|
|
|
Paragraph,
|
|
|
|
Bold,
|
|
|
|
Italic,
|
|
|
|
Link.configure({
|
|
|
|
openOnClick: false
|
|
|
|
}),
|
|
|
|
Blockquote.configure({
|
|
|
|
HTMLAttributes: {
|
|
|
|
class: styles.blockQuote
|
|
|
|
}
|
|
|
|
}),
|
2023-08-15 09:38:49 +00:00
|
|
|
ImageFigure,
|
2023-08-17 10:36:27 +00:00
|
|
|
Image,
|
|
|
|
Figcaption,
|
2023-07-24 08:58:07 +00:00
|
|
|
Placeholder.configure({
|
|
|
|
emptyNodeClass: styles.emptyNode,
|
|
|
|
placeholder: props.placeholder
|
|
|
|
})
|
|
|
|
],
|
|
|
|
content: props.initialContent ?? null
|
|
|
|
}))
|
|
|
|
|
|
|
|
setEditor(editor)
|
|
|
|
const isEmpty = useEditorIsEmpty(() => editor())
|
|
|
|
const isFocused = useEditorIsFocused(() => editor())
|
|
|
|
|
|
|
|
const isActive = (name: string) =>
|
|
|
|
createEditorTransaction(
|
|
|
|
() => editor(),
|
|
|
|
(ed) => {
|
|
|
|
return ed && ed.isActive(name)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const html = useEditorHTML(() => editor())
|
|
|
|
const isBold = isActive('bold')
|
|
|
|
const isItalic = isActive('italic')
|
|
|
|
const isLink = isActive('link')
|
|
|
|
const isBlockquote = isActive('blockquote')
|
|
|
|
|
2023-08-15 09:38:49 +00:00
|
|
|
const renderImage = (image: UploadedFile) => {
|
2023-07-24 08:58:07 +00:00
|
|
|
editor()
|
|
|
|
.chain()
|
|
|
|
.focus()
|
2023-08-15 09:38:49 +00:00
|
|
|
.insertContent({
|
|
|
|
type: 'capturedImage',
|
|
|
|
content: [
|
|
|
|
{
|
|
|
|
type: 'figcaption',
|
|
|
|
content: [
|
|
|
|
{
|
|
|
|
type: 'text',
|
|
|
|
text: image.originalFilename
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'image',
|
|
|
|
attrs: {
|
|
|
|
src: imageProxy(image.url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
2023-07-24 08:58:07 +00:00
|
|
|
.run()
|
|
|
|
hideModal()
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleClear = () => {
|
|
|
|
editor().commands.clearContent(true)
|
|
|
|
}
|
2023-08-13 21:26:40 +00:00
|
|
|
|
2023-07-24 08:58:07 +00:00
|
|
|
createEffect(() => {
|
|
|
|
if (props.setClear) {
|
|
|
|
editor().commands.clearContent(true)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const handleKeyDown = async (event) => {
|
2023-08-13 21:26:40 +00:00
|
|
|
if (isEmpty() || !isFocused()) {
|
2023-07-28 09:47:19 +00:00
|
|
|
return
|
2023-07-24 08:58:07 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 09:47:19 +00:00
|
|
|
if (
|
|
|
|
event.code === 'Enter' &&
|
|
|
|
((props.submitByEnter && !event.shiftKey) || (props.submitByShiftEnter && event.shiftKey))
|
|
|
|
) {
|
2023-07-24 08:58:07 +00:00
|
|
|
event.preventDefault()
|
|
|
|
props.onSubmit(html())
|
|
|
|
handleClear()
|
|
|
|
}
|
2023-07-28 09:47:19 +00:00
|
|
|
|
|
|
|
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
2023-07-28 19:53:21 +00:00
|
|
|
event.preventDefault()
|
2023-07-28 09:47:19 +00:00
|
|
|
showModal('editorInsertLink')
|
|
|
|
}
|
2023-07-24 08:58:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onMount(() => {
|
2023-07-27 17:38:38 +00:00
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
2023-07-24 08:58:07 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
onCleanup(() => {
|
|
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
|
|
})
|
2023-07-28 19:53:21 +00:00
|
|
|
|
2023-08-17 10:36:27 +00:00
|
|
|
if (props.onChange) {
|
2023-08-13 21:26:40 +00:00
|
|
|
createEffect(() => {
|
2023-08-17 10:36:27 +00:00
|
|
|
props.onChange(html())
|
2023-08-13 21:26:40 +00:00
|
|
|
})
|
|
|
|
}
|
2023-08-17 10:36:27 +00:00
|
|
|
|
2023-07-28 09:47:19 +00:00
|
|
|
const handleInsertLink = () => !editor().state.selection.empty && showModal('editorInsertLink')
|
2023-08-13 21:26:40 +00:00
|
|
|
|
2023-07-24 08:58:07 +00:00
|
|
|
return (
|
|
|
|
<div
|
2023-08-17 10:36:27 +00:00
|
|
|
ref={(el) => (wrapperEditorElRef.current = el)}
|
2023-07-24 08:58:07 +00:00
|
|
|
class={clsx(styles.SimplifiedEditor, {
|
|
|
|
[styles.smallHeight]: props.smallHeight,
|
|
|
|
[styles.isFocused]: isFocused() || !isEmpty()
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<div ref={(el) => (editorElRef.current = el)} />
|
|
|
|
<div class={styles.controls}>
|
|
|
|
<div class={styles.actions}>
|
|
|
|
<Popover content={t('Bold')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.actionButton, { [styles.active]: isBold() })}
|
|
|
|
onClick={() => editor().chain().focus().toggleBold().run()}
|
|
|
|
>
|
|
|
|
<Icon name="editor-bold" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Popover content={t('Italic')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.actionButton, { [styles.active]: isItalic() })}
|
|
|
|
onClick={() => editor().chain().focus().toggleItalic().run()}
|
|
|
|
>
|
|
|
|
<Icon name="editor-italic" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Popover content={t('Add url')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
2023-07-28 19:53:21 +00:00
|
|
|
onClick={handleInsertLink}
|
2023-07-24 08:58:07 +00:00
|
|
|
class={clsx(styles.actionButton, { [styles.active]: isLink() })}
|
|
|
|
>
|
|
|
|
<Icon name="editor-link" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Show when={props.quoteEnabled}>
|
|
|
|
<Popover content={t('Add blockquote')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
2023-07-28 19:53:21 +00:00
|
|
|
onClick={() => editor().chain().focus().toggleBlockquote().run()}
|
2023-07-24 08:58:07 +00:00
|
|
|
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
|
|
|
>
|
|
|
|
<Icon name="editor-quote" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
</Show>
|
|
|
|
<Show when={props.imageEnabled}>
|
|
|
|
<Popover content={t('Add image')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
onClick={() => showModal('uploadImage')}
|
|
|
|
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
|
|
|
>
|
|
|
|
<Icon name="editor-image-dd-full" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
</Show>
|
|
|
|
</div>
|
2023-08-17 10:36:27 +00:00
|
|
|
<Show when={!props.onChange}>
|
2023-08-13 21:26:40 +00:00
|
|
|
<div class={styles.buttons}>
|
|
|
|
<Button value={t('Cancel')} variant="secondary" disabled={isEmpty()} onClick={handleClear} />
|
|
|
|
<Button
|
|
|
|
value={props.submitButtonText ?? t('Send')}
|
|
|
|
variant="primary"
|
|
|
|
disabled={isEmpty()}
|
|
|
|
onClick={() => props.onSubmit(html())}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</Show>
|
2023-07-24 08:58:07 +00:00
|
|
|
</div>
|
|
|
|
<Modal variant="narrow" name="editorInsertLink">
|
2023-07-28 19:53:21 +00:00
|
|
|
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
2023-07-24 08:58:07 +00:00
|
|
|
</Modal>
|
|
|
|
<Show when={props.imageEnabled}>
|
|
|
|
<Modal variant="narrow" name="uploadImage">
|
|
|
|
<UploadModalContent
|
|
|
|
onClose={(value) => {
|
|
|
|
renderImage(value)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default SimplifiedEditor // "export default" need to use for asynchronous (lazy) imports in the comments tree
|