mini+micro-fix
This commit is contained in:
parent
21b3903062
commit
abdc419aa8
|
@ -1,138 +0,0 @@
|
|||
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 { UploadedFile } from '~/types/upload'
|
||||
import { InsertLinkForm } from './InsertLinkForm'
|
||||
import { ToolbarControl as Control } from './ToolbarControl'
|
||||
|
||||
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||
|
||||
interface EditorToolbarProps {
|
||||
editor: Accessor<Editor | undefined>
|
||||
mode?: 'micro' | 'mini'
|
||||
}
|
||||
|
||||
export const EditorToolbar = (props: EditorToolbarProps) => {
|
||||
const { t } = useLocalize()
|
||||
const { showModal } = useUI()
|
||||
|
||||
// show / hide for link input
|
||||
const [showLinkInput, setShowLinkInput] = createSignal(false)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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 (
|
||||
<div style={{ 'background-color': 'white', display: 'inline-flex' }}>
|
||||
<Show
|
||||
when={((props.mode === 'micro' && showSimpleMenu()) || props.mode !== 'micro') && props.editor()}
|
||||
keyed
|
||||
>
|
||||
{(instance) => (
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -5,8 +5,11 @@ import { validateUrl } from '~/utils/validate'
|
|||
import { InlineForm } from '../../_shared/InlineForm'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
editor: Editor
|
||||
onClose: () => void
|
||||
onSubmit?: (value: string) => void
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
export const checkUrl = (url: string) => {
|
||||
|
@ -38,7 +41,7 @@ export const InsertLinkForm = (props: Props) => {
|
|||
|
||||
const handleClearLinkForm = () => {
|
||||
if (currentUrl()) {
|
||||
props.editor?.chain().focus().unsetLink().run()
|
||||
props.onRemove?.()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,11 +51,12 @@ export const InsertLinkForm = (props: Props) => {
|
|||
.focus()
|
||||
.setLink({ href: checkUrl(value) })
|
||||
.run()
|
||||
props.onSubmit?.(value)
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class={props.class}>
|
||||
<InlineForm
|
||||
placeholder={t('Enter URL address')}
|
||||
initialValue={currentUrl() ?? ''}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Popover } from '~/components/_shared/Popover'
|
|||
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||
|
||||
interface ControlProps {
|
||||
editor: Editor
|
||||
editor: Editor | undefined
|
||||
title: string
|
||||
key: string
|
||||
onChange: () => void
|
||||
|
@ -27,7 +27,7 @@ export const ToolbarControl = (props: ControlProps): JSX.Element => {
|
|||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.actionButton, { [styles.active]: props.editor.isActive(props.key) })}
|
||||
class={clsx(styles.actionButton, { [styles.active]: props.editor?.isActive?.(props.key) })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu'
|
||||
import clsx from 'clsx'
|
||||
import { type JSX, createEffect, createSignal, on } from 'solid-js'
|
||||
import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap'
|
||||
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap'
|
||||
import { minimal } from '~/lib/editorExtensions'
|
||||
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
||||
import { Editor } from '@tiptap/core'
|
||||
|
||||
import { Icon } from '~/components/_shared/Icon/Icon'
|
||||
import { ToolbarControl as Control } from '../EditorToolbar/ToolbarControl'
|
||||
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||
|
||||
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
|
||||
interface MicroEditorProps {
|
||||
content?: string
|
||||
onChange?: (content: string) => void
|
||||
onSubmit?: (content: string) => void
|
||||
placeholder?: string
|
||||
bordered?: boolean
|
||||
}
|
||||
|
||||
export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
|
||||
const { t } = useLocalize()
|
||||
const [showLinkForm, setShowLinkForm] = createSignal(false)
|
||||
const [isActive, setIsActive] = createSignal(false)
|
||||
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
||||
|
||||
const editor = createTiptapEditor(() => ({
|
||||
element: editorElement()!,
|
||||
extensions: [
|
||||
|
@ -28,21 +37,100 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
|
|||
class: styles.simplifiedEditorField
|
||||
}
|
||||
},
|
||||
content: props.content || ''
|
||||
content: props.content || '',
|
||||
autofocus: 'end'
|
||||
}))
|
||||
|
||||
const isFocused = useEditorIsFocused(editor)
|
||||
const selection = createEditorTransaction(editor, (e?: Editor) => e?.state.selection)
|
||||
const html = useEditorHTML(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()
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
||||
|
||||
createEffect(() => {
|
||||
const updateActive = () => {
|
||||
setIsActive(Boolean(selection()) || editor()?.isActive('link') || showLinkForm())
|
||||
}
|
||||
updateActive()
|
||||
editor()?.on('focus', updateActive)
|
||||
editor()?.on('blur', updateActive)
|
||||
return () => {
|
||||
editor()?.off('focus', updateActive)
|
||||
editor()?.off('blur', updateActive)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.MiniEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||
<div>
|
||||
<EditorToolbar editor={editor} mode={'micro'} />
|
||||
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
||||
<div class={clsx(
|
||||
styles.MiniEditor, {
|
||||
[styles.bordered]: props.bordered,
|
||||
[styles.isFocused]: isActive() && selection() && Boolean(!selection()?.empty)
|
||||
})}>
|
||||
<div class={styles.controls}>
|
||||
<div class={styles.actions}>
|
||||
<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>
|
||||
<InsertLinkForm
|
||||
class={clsx([styles.linkInput, { [styles.linkInputactive]: showLinkForm() }])}
|
||||
editor={editor() as Editor}
|
||||
onClose={toggleLinkForm}
|
||||
onSubmit={setLink}
|
||||
onRemove={removeLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MicroEditor
|
||||
export default MicroEditor
|
|
@ -44,6 +44,14 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bubbleMenu {
|
||||
display: flex;
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: auto;
|
||||
|
@ -89,6 +97,15 @@
|
|||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.linkInput {
|
||||
opacity: 0;
|
||||
transition: opacity ease-in-out 0.3s;
|
||||
&.linkInputactive {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,22 @@ import CharacterCount from '@tiptap/extension-character-count'
|
|||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import clsx from 'clsx'
|
||||
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML, useEditorIsEmpty } from 'solid-tiptap'
|
||||
import { Button } from '~/components/_shared/Button'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
import { base } from '~/lib/editorExtensions'
|
||||
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
||||
import { ToolbarControl as Control } from '../EditorToolbar/ToolbarControl'
|
||||
|
||||
import styles from './MiniEditor.module.scss'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||
import { Icon } from '~/components/_shared/Icon/Icon'
|
||||
import { useUI } from '~/context/ui'
|
||||
import { Modal } from '~/components/_shared/Modal'
|
||||
import { UploadModalContent } from '~/components/Upload/UploadModalContent'
|
||||
import { Portal } from 'solid-js/web'
|
||||
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
||||
import { UploadedFile } from '~/types/upload'
|
||||
|
||||
interface MiniEditorProps {
|
||||
content?: string
|
||||
|
@ -21,9 +30,10 @@ interface MiniEditorProps {
|
|||
|
||||
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||
const { t } = useLocalize()
|
||||
const { showModal } = useUI()
|
||||
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
||||
const [counter, setCounter] = createSignal(0)
|
||||
|
||||
const [showLinkForm, setShowLinkForm] = createSignal(false)
|
||||
const editor = createTiptapEditor(() => ({
|
||||
element: editorElement()!,
|
||||
extensions: [
|
||||
|
@ -36,12 +46,39 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
|||
class: styles.simplifiedEditorField
|
||||
}
|
||||
},
|
||||
content: props.content || ''
|
||||
content: props.content || '',
|
||||
autofocus: 'end'
|
||||
}))
|
||||
|
||||
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
|
||||
const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty)
|
||||
const html = useEditorHTML(editor)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
||||
|
||||
|
@ -58,28 +95,85 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.MiniEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||
<div>
|
||||
<div id="mini-editor" ref={setEditorElement} />
|
||||
|
||||
<EditorToolbar editor={editor} mode={'mini'} />
|
||||
|
||||
<div class={styles.buttons}>
|
||||
<Button
|
||||
value={t('Cancel')}
|
||||
disabled={isEmpty()}
|
||||
variant="secondary"
|
||||
onClick={() => editor()?.commands.clearContent()}
|
||||
<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}
|
||||
/>
|
||||
<Button value={t('Send')} variant="primary" disabled={isEmpty()} onClick={handleSubmit} />
|
||||
</div>
|
||||
|
||||
<Show when={counter() > 0}>
|
||||
<small class={styles.limit}>
|
||||
{counter()} / {props.limit || '∞'}
|
||||
</small>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
<Show when={counter() > 0}>
|
||||
<small class={styles.limit}>
|
||||
{counter()} / {props.limit || '∞'}
|
||||
</small>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -339,7 +339,7 @@ export const ProfileSettings = () => {
|
|||
/>
|
||||
|
||||
<h4>{t('About')}</h4>
|
||||
<MicroEditor content={about() || ''} onChange={setAbout} placeholder={t('About')} />
|
||||
<MicroEditor content={about() || ''} onChange={setAbout} placeholder={t('About')} bordered={true} />
|
||||
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
|
||||
<div class={styles.multipleControlsHeader}>
|
||||
<h4>{t('Social networks')}</h4>
|
||||
|
|
Loading…
Reference in New Issue
Block a user