bubble-menu-used
This commit is contained in:
parent
67e8c80d9a
commit
ae1a93469b
|
@ -5,7 +5,7 @@ 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 '../EditorToolbar/InsertLinkForm'
|
||||
import { InsertLinkForm } from '../Toolbar/InsertLinkForm'
|
||||
|
||||
import styles from './TextBubbleMenu.module.scss'
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
.MicroBubbleMenu {
|
||||
display: flex;
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 0 1px rgb(0 0 0 / 5%), 0 10px 20px rgb(0 0 0 / 10%);
|
||||
|
||||
.bubbleMenuButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover,
|
||||
&.bubbleMenuButtonActive {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.noWrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
100
src/components/Editor/MicroEditor/MicroBubbleMenu.tsx
Normal file
100
src/components/Editor/MicroEditor/MicroBubbleMenu.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createEffect, createSignal } 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 '../Toolbar/InsertLinkForm'
|
||||
|
||||
import styles from './MicroBubbleMenu.module.scss'
|
||||
|
||||
type MicroBubbleMenuProps = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLDivElement) => void
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export const MicroBubbleMenu = (props: MicroBubbleMenuProps) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const isActive = (name: string, attributes?: Record<string, string | number>) =>
|
||||
createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor?.isActive(name, attributes)
|
||||
)
|
||||
|
||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
||||
createEffect(() => props.hidden && setLinkEditorOpen(false))
|
||||
|
||||
const isBold = isActive('bold')
|
||||
const isItalic = isActive('italic')
|
||||
const isLink = isActive('link')
|
||||
|
||||
const handleOpenLinkForm = () => {
|
||||
const { from, to } = props.editor.state.selection
|
||||
props.editor?.chain().focus().setTextSelection({ from, to }).run()
|
||||
setLinkEditorOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseLinkForm = () => {
|
||||
setLinkEditorOpen(false)
|
||||
// Снимаем выделение, устанавливая курсор в конец текущего выделения
|
||||
const { to } = props.editor.state.selection
|
||||
props.editor?.chain().focus().setTextSelection(to).run()
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={props.ref} class={styles.MicroBubbleMenu}>
|
||||
<Show
|
||||
when={!linkEditorOpen()}
|
||||
fallback={<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />}
|
||||
>
|
||||
<Popover content={t('Bold')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBold()
|
||||
})}
|
||||
onClick={() => props.editor?.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Icon name="editor-bold" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Italic')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isItalic()
|
||||
})}
|
||||
onClick={() => props.editor?.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Icon name="editor-italic" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={handleOpenLinkForm}
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isLink()
|
||||
})}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MicroBubbleMenu
|
|
@ -1,15 +1,11 @@
|
|||
import { Editor } from '@tiptap/core'
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import clsx from 'clsx'
|
||||
import { type JSX, createEffect, createSignal, on } from 'solid-js'
|
||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import { minimal } from '~/lib/editorExtensions'
|
||||
import { MicroBubbleMenu } from './MicroBubbleMenu'
|
||||
|
||||
import { Icon } from '~/components/_shared/Icon/Icon'
|
||||
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||
import { ToolbarControl as Control } from '../EditorToolbar/ToolbarControl'
|
||||
|
||||
import { useLocalize } from '~/context/localize'
|
||||
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||
|
||||
interface MicroEditorProps {
|
||||
|
@ -21,15 +17,18 @@ interface MicroEditorProps {
|
|||
}
|
||||
|
||||
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 [bubbleMenuElement, setBubbleMenuElement] = createSignal<HTMLDivElement>()
|
||||
|
||||
const editor = createTiptapEditor(() => ({
|
||||
element: editorElement()!,
|
||||
extensions: [
|
||||
...minimal,
|
||||
Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder })
|
||||
Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }),
|
||||
BubbleMenu.configure({
|
||||
element: bubbleMenuElement()!,
|
||||
shouldShow: ({ state: { selection }, editor }) => !selection.empty || editor.isActive('link')
|
||||
})
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
|
@ -40,94 +39,21 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
|
|||
autofocus: 'end'
|
||||
}))
|
||||
|
||||
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]: props.bordered,
|
||||
[styles.isFocused]: isActive() && selection() && Boolean(!selection()?.empty)
|
||||
[styles.bordered]: props.bordered
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
<MicroBubbleMenu
|
||||
editor={editor()!}
|
||||
ref={setBubbleMenuElement}
|
||||
hidden={!!editor()?.state.selection.empty}
|
||||
/>
|
||||
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import { createTiptapEditor, useEditorHTML, useEditorIsEmpty } from 'solid-tipta
|
|||
import { Button } from '~/components/_shared/Button'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
import { base } from '~/lib/editorExtensions'
|
||||
import { ToolbarControl as Control } from '../EditorToolbar/ToolbarControl'
|
||||
import { ToolbarControl as Control } from '../Toolbar/ToolbarControl'
|
||||
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { Portal } from 'solid-js/web'
|
||||
|
@ -16,7 +16,7 @@ import { Icon } from '~/components/_shared/Icon/Icon'
|
|||
import { Modal } from '~/components/_shared/Modal'
|
||||
import { useUI } from '~/context/ui'
|
||||
import { UploadedFile } from '~/types/upload'
|
||||
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||
import { InsertLinkForm } from '../Toolbar/InsertLinkForm'
|
||||
import styles from './MiniEditor.module.scss'
|
||||
|
||||
interface MiniEditorProps {
|
||||
|
|
Loading…
Reference in New Issue
Block a user