editor-toolbar
This commit is contained in:
parent
76dea4341d
commit
90cd3988a1
|
@ -154,10 +154,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MiniEditor
|
<MiniEditor placeholder={t('Write a comment...')} onSubmit={handleSubmitComment} />
|
||||||
placeholder={t('Write a comment...')}
|
|
||||||
onSubmit={handleSubmitComment}
|
|
||||||
/>
|
|
||||||
<Show when={posting()}>
|
<Show when={posting()}>
|
||||||
<Loading />
|
<Loading />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import { Accessor, Show, createEffect, createSignal, on } from 'solid-js'
|
import { Accessor, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
|
import { Portal } from 'solid-js/web'
|
||||||
import { createEditorTransaction } from 'solid-tiptap'
|
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 { Icon } from '~/components/_shared/Icon/Icon'
|
||||||
|
import { Modal } from '~/components/_shared/Modal/Modal'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useUI } from '~/context/ui'
|
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 { ToolbarControl as Control } from './ToolbarControl'
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
import styles from '../SimplifiedEditor.module.scss'
|
||||||
|
|
||||||
interface MiniToolbarProps {
|
interface EditorToolbarProps {
|
||||||
editor: Accessor<Editor | undefined>
|
editor: Accessor<Editor | undefined>
|
||||||
|
mode?: 'micro' | 'mini'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MiniToolbar = (props: MiniToolbarProps) => {
|
export const EditorToolbar = (props: EditorToolbarProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { showModal } = useUI()
|
const { showModal } = useUI()
|
||||||
|
|
||||||
|
@ -23,23 +29,24 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
|
||||||
// focus on link input when it shows up
|
// focus on link input when it shows up
|
||||||
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run()))
|
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run()))
|
||||||
|
|
||||||
const selection = createEditorTransaction(
|
const selection = createEditorTransaction(props.editor, (instance) => instance?.state.selection)
|
||||||
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 [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
|
||||||
const recoverSelection = () => {
|
const recoverSelection = () => {
|
||||||
if (!storedSelection()?.empty) {
|
if (!storedSelection()?.empty) {
|
||||||
createEditorTransaction(
|
createEditorTransaction(props.editor, (instance?: Editor) => {
|
||||||
props.editor,
|
|
||||||
(instance?: Editor) => {
|
|
||||||
const r = selection()
|
const r = selection()
|
||||||
if (instance && r) {
|
if (instance && r) {
|
||||||
instance.state.selection.from === r.from
|
instance.state.selection.from === r.from
|
||||||
instance.state.selection.to === r.to
|
instance.state.selection.to === r.to
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const storeSelection = () => {
|
const storeSelection = () => {
|
||||||
|
@ -60,7 +67,10 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ 'background-color': 'white', display: 'inline-flex' }}>
|
<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) => (
|
{(instance) => (
|
||||||
<div class={styles.controls}>
|
<div class={styles.controls}>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
|
@ -89,6 +99,7 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
|
||||||
>
|
>
|
||||||
<Icon name="editor-link" />
|
<Icon name="editor-link" />
|
||||||
</Control>
|
</Control>
|
||||||
|
<Show when={props.mode !== 'micro'}>
|
||||||
<Control
|
<Control
|
||||||
key="blockquote"
|
key="blockquote"
|
||||||
editor={instance}
|
editor={instance}
|
||||||
|
@ -105,10 +116,20 @@ export const MiniToolbar = (props: MiniToolbarProps) => {
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-dd-full" />
|
<Icon name="editor-image-dd-full" />
|
||||||
</Control>
|
</Control>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={showLinkInput()}>
|
<Show when={showLinkInput()}>
|
||||||
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
|
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Portal>
|
||||||
|
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
||||||
|
<UploadModalContent
|
||||||
|
onClose={(image) => renderUploadedImage(instance as Editor, image as UploadedFile)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Portal>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { InsertLinkForm } from './InsertLinkForm'
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { type JSX, createEffect, createSignal, on } from 'solid-js'
|
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 { minimal } from '~/lib/editorExtensions'
|
||||||
import { MicroToolbar } from '../EditorToolbar/MicroToolbar'
|
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
import styles from '../SimplifiedEditor.module.scss'
|
||||||
|
|
||||||
|
@ -31,20 +31,14 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
|
||||||
content: props.content || ''
|
content: props.content || ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const isEmpty = useEditorIsEmpty(editor)
|
|
||||||
const isFocused = useEditorIsFocused(editor)
|
const isFocused = useEditorIsFocused(editor)
|
||||||
const html = useEditorHTML(editor)
|
const html = useEditorHTML(editor)
|
||||||
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||||
class={clsx(styles.SimplifiedEditor, styles.bordered, {
|
|
||||||
[styles.isFocused]: isEmpty() || isFocused()
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<MicroToolbar editor={editor} />
|
<EditorToolbar editor={editor} mode={'micro'} />
|
||||||
|
|
||||||
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,10 @@ import clsx from 'clsx'
|
||||||
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
import { base } from '~/lib/editorExtensions'
|
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'
|
import styles from '../SimplifiedEditor.module.scss'
|
||||||
|
|
||||||
interface MiniEditorProps {
|
interface MiniEditorProps {
|
||||||
|
@ -18,6 +20,7 @@ interface MiniEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||||
|
const { t } = useLocalize()
|
||||||
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
||||||
const [counter, setCounter] = createSignal(0)
|
const [counter, setCounter] = createSignal(0)
|
||||||
|
|
||||||
|
@ -36,7 +39,10 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||||
content: props.content || ''
|
content: props.content || ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
|
||||||
|
const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty)
|
||||||
const html = useEditorHTML(editor)
|
const html = useEditorHTML(editor)
|
||||||
|
|
||||||
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -46,14 +52,27 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||||
content && props.onChange?.(content)
|
content && props.onChange?.(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
|
const handleSubmit = () => {
|
||||||
|
html() && props.onSubmit?.(html() || '')
|
||||||
|
editor()?.commands.clearContent(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||||
<div>
|
<div>
|
||||||
<div id="mini-editor" ref={setEditorElement} />
|
<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}>
|
<Show when={counter() > 0}>
|
||||||
<small class={styles.limit}>
|
<small class={styles.limit}>
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
||||||
import { createEditorTransaction } from 'solid-tiptap'
|
import { createEditorTransaction } from 'solid-tiptap'
|
||||||
|
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
import { Popover } from '~/components/_shared/Popover'
|
import { Popover } from '~/components/_shared/Popover'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { InsertLinkForm } from '../InsertLinkForm'
|
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||||
|
|
||||||
import styles from './TextBubbleMenu.module.scss'
|
import styles from './TextBubbleMenu.module.scss'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user