From ae1a93469b8bb8c35a1ae72e4d302ba63892bcaa Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 1 Oct 2024 21:11:07 +0300 Subject: [PATCH] bubble-menu-used --- .../Editor/BubbleMenu/TextBubbleMenu.tsx | 2 +- .../MicroEditor/MicroBubbleMenu.module.scss | 29 +++++ .../Editor/MicroEditor/MicroBubbleMenu.tsx | 100 +++++++++++++++++ .../Editor/MicroEditor/MicroEditor.tsx | 106 +++--------------- .../Editor/MiniEditor/MiniEditor.tsx | 4 +- .../InsertLinkForm.tsx | 0 .../ToolbarControl.tsx | 0 7 files changed, 148 insertions(+), 93 deletions(-) create mode 100644 src/components/Editor/MicroEditor/MicroBubbleMenu.module.scss create mode 100644 src/components/Editor/MicroEditor/MicroBubbleMenu.tsx rename src/components/Editor/{EditorToolbar => Toolbar}/InsertLinkForm.tsx (100%) rename src/components/Editor/{EditorToolbar => Toolbar}/ToolbarControl.tsx (100%) diff --git a/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx index e71b3575..5b8cf4b6 100644 --- a/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx @@ -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' diff --git a/src/components/Editor/MicroEditor/MicroBubbleMenu.module.scss b/src/components/Editor/MicroEditor/MicroBubbleMenu.module.scss new file mode 100644 index 00000000..e4e149f0 --- /dev/null +++ b/src/components/Editor/MicroEditor/MicroBubbleMenu.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; + } + } \ No newline at end of file diff --git a/src/components/Editor/MicroEditor/MicroBubbleMenu.tsx b/src/components/Editor/MicroEditor/MicroBubbleMenu.tsx new file mode 100644 index 00000000..cc1a0b95 --- /dev/null +++ b/src/components/Editor/MicroEditor/MicroBubbleMenu.tsx @@ -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) => + 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 ( +
+ } + > + + {(triggerRef: (el: HTMLElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLElement) => void) => ( + + )} + + {t('Add url')}
}> + {(triggerRef: (el: HTMLElement) => void) => ( + + )} + + + + ) +} + +export default MicroBubbleMenu diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index d7f30f92..86680d17 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -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() + const [bubbleMenuElement, setBubbleMenuElement] = createSignal() + 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 (
-
-
- editor()?.chain().focus().toggleBold().run()} - title={t('Bold')} - > - - - editor()?.chain().focus().toggleItalic().run()} - title={t('Italic')} - > - - - Boolean(e?.isActive('link'))} - > - - - -
-
+