editor-toolbar

This commit is contained in:
Untone 2024-09-27 20:57:25 +03:00
parent 76dea4341d
commit 90cd3988a1
8 changed files with 81 additions and 166 deletions

View File

@ -154,10 +154,7 @@ export const CommentsTree = (props: Props) => {
</div>
}
>
<MiniEditor
placeholder={t('Write a comment...')}
onSubmit={handleSubmitComment}
/>
<MiniEditor placeholder={t('Write a comment...')} onSubmit={handleSubmitComment} />
<Show when={posting()}>
<Loading />
</Show>

View File

@ -1,19 +1,25 @@
import { Editor } from '@tiptap/core'
import { Accessor, Show, createEffect, createSignal, on } from 'solid-js'
import { Portal } from 'solid-js/web'
import { createEditorTransaction } from 'solid-tiptap'
import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent'
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
import { Icon } from '~/components/_shared/Icon/Icon'
import { Modal } from '~/components/_shared/Modal/Modal'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { UploadedFile } from '~/types/upload'
import { InsertLinkForm } from './InsertLinkForm'
import { ToolbarControl as Control } from './ToolbarControl'
import styles from '../SimplifiedEditor.module.scss'
interface MiniToolbarProps {
interface EditorToolbarProps {
editor: Accessor<Editor | undefined>
mode?: 'micro' | 'mini'
}
export const MiniToolbar = (props: MiniToolbarProps) => {
export const EditorToolbar = (props: EditorToolbarProps) => {
const { t } = useLocalize()
const { showModal } = useUI()
@ -23,23 +29,24 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
// focus on link input when it shows up
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run()))
const selection = createEditorTransaction(
props.editor,
(instance) => instance?.state.selection
const selection = createEditorTransaction(props.editor, (instance) => instance?.state.selection)
// change visibility on selection if not in link input mode
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
createEffect(
on([selection, showLinkInput], ([s, l]) => props.mode === 'micro' && !l && setShowSimpleMenu(!s?.empty))
)
const [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
const recoverSelection = () => {
if (!storedSelection()?.empty) {
createEditorTransaction(
props.editor,
(instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
createEditorTransaction(props.editor, (instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
)
})
}
}
const storeSelection = () => {
@ -60,7 +67,10 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
return (
<div style={{ 'background-color': 'white', display: 'inline-flex' }}>
<Show when={props.editor()} keyed>
<Show
when={((props.mode === 'micro' && showSimpleMenu()) || props.mode !== 'micro') && props.editor()}
keyed
>
{(instance) => (
<div class={styles.controls}>
<div class={styles.actions}>
@ -89,26 +99,37 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
>
<Icon name="editor-link" />
</Control>
<Control
key="blockquote"
editor={instance}
onChange={() => instance.chain().focus().toggleBlockquote().run()}
title={t('Add blockquote')}
>
<Icon name="editor-quote" />
</Control>
<Control
key="image"
editor={instance}
onChange={() => showModal('simplifiedEditorUploadImage')}
title={t('Add image')}
>
<Icon name="editor-image-dd-full" />
</Control>
<Show when={props.mode !== 'micro'}>
<Control
key="blockquote"
editor={instance}
onChange={() => instance.chain().focus().toggleBlockquote().run()}
title={t('Add blockquote')}
>
<Icon name="editor-quote" />
</Control>
<Control
key="image"
editor={instance}
onChange={() => showModal('simplifiedEditorUploadImage')}
title={t('Add image')}
>
<Icon name="editor-image-dd-full" />
</Control>
</Show>
</div>
<Show when={showLinkInput()}>
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
</Show>
<Portal>
<Modal variant="narrow" name="simplifiedEditorUploadImage">
<UploadModalContent
onClose={(image) => renderUploadedImage(instance as Editor, image as UploadedFile)}
/>
</Modal>
</Portal>
</div>
)}
</Show>

View File

@ -1,113 +0,0 @@
import { Editor } from '@tiptap/core'
import { Accessor, Show, createEffect, createSignal, on } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon/Icon'
import { useLocalize } from '~/context/localize'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { ToolbarControl as Control } from './ToolbarControl'
import styles from '../SimplifiedEditor.module.scss'
export interface MicroToolbarProps {
showing?: boolean
editor: Accessor<Editor|undefined>
}
export const MicroToolbar = (props: MicroToolbarProps) => {
const { t } = useLocalize()
// show / hide for menu
const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing)
const selection = createEditorTransaction(
props.editor,
(instance) => instance?.state.selection
)
// show / hide for link input
const [showLinkInput, setShowLinkInput] = createSignal(false)
// change visibility on selection if not in link input mode
createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty)))
// focus on link input when it shows up
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run()))
const [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
const recoverSelection = () => {
if (!storedSelection()?.empty) {
createEditorTransaction(
props.editor,
(instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
}
)
}
}
const storeSelection = () => {
const selection = props.editor()?.state.selection
if (!selection?.empty) {
setStoredSelection(selection)
}
}
const toggleShowLink = () => {
if (showLinkInput()) {
props.editor()?.chain().focus().run()
recoverSelection()
} else {
storeSelection()
}
setShowLinkInput(!showLinkInput())
}
return (
<Show when={props.editor()} keyed>
{(instance) => (
<Show when={!showSimpleMenu()}>
<div
style={{
display: 'inline-flex',
background: 'var(--editor-bubble-menu-background)',
border: '1px solid black'
}}
>
<div class={styles.controls}>
<div class={styles.actions}>
<Control
key="bold"
editor={instance}
onChange={() => instance.chain().focus().toggleBold().run()}
title={t('Bold')}
>
<Icon name="editor-bold" />
</Control>
<Control
key="italic"
editor={instance}
onChange={() => instance.chain().focus().toggleItalic().run()}
title={t('Italic')}
>
<Icon name="editor-italic" />
</Control>
<Control
key="link"
editor={instance}
onChange={toggleShowLink}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
</div>
<Show when={showLinkInput()}>
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
</Show>
</div>
</div>
</Show>
)}
</Show>
)
}

View File

@ -1 +0,0 @@
export { InsertLinkForm } from './InsertLinkForm'

View File

@ -1,9 +1,9 @@
import Placeholder from '@tiptap/extension-placeholder'
import clsx from 'clsx'
import { type JSX, createEffect, createSignal, on } from 'solid-js'
import { createTiptapEditor, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap'
import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap'
import { minimal } from '~/lib/editorExtensions'
import { MicroToolbar } from '../EditorToolbar/MicroToolbar'
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
import styles from '../SimplifiedEditor.module.scss'
@ -31,20 +31,14 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
content: props.content || ''
}))
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const html = useEditorHTML(editor)
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
return (
<div
class={clsx(styles.SimplifiedEditor, styles.bordered, {
[styles.isFocused]: isEmpty() || isFocused()
})}
>
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
<div>
<MicroToolbar editor={editor} />
<EditorToolbar editor={editor} mode={'micro'} />
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
</div>
</div>

View File

@ -4,8 +4,10 @@ import clsx from 'clsx'
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { base } from '~/lib/editorExtensions'
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
import { MiniToolbar } from '../EditorToolbar/MiniToolbar'
import { Button } from '~/components/_shared/Button'
import { useLocalize } from '~/context/localize'
import styles from '../SimplifiedEditor.module.scss'
interface MiniEditorProps {
@ -18,6 +20,7 @@ interface MiniEditorProps {
}
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
const { t } = useLocalize()
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [counter, setCounter] = createSignal(0)
@ -36,7 +39,10 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content: props.content || ''
}))
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty)
const html = useEditorHTML(editor)
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(() => {
@ -46,14 +52,27 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content && props.onChange?.(content)
})
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
const handleSubmit = () => {
html() && props.onSubmit?.(html() || '')
editor()?.commands.clearContent(true)
}
return (
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
<div>
<div id="mini-editor" ref={setEditorElement} />
<MiniToolbar editor={editor} />
<EditorToolbar editor={editor} mode={'mini'} />
<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} />
</div>
<Show when={counter() > 0}>
<small class={styles.limit}>

View File

@ -1,13 +1,11 @@
import type { Editor } from '@tiptap/core'
import { clsx } from 'clsx'
import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon'
import { Popover } from '~/components/_shared/Popover'
import { useLocalize } from '~/context/localize'
import { InsertLinkForm } from '../InsertLinkForm'
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
import styles from './TextBubbleMenu.module.scss'