popper-fix+textbubble-revert
This commit is contained in:
parent
cd39b7b806
commit
d28b4f2ab3
|
@ -133,12 +133,7 @@
|
|||
"yjs": "13.6.19",
|
||||
"y-prosemirror": "1.2.12"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome",
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"protobufjs"
|
||||
],
|
||||
"trustedDependencies": ["@biomejs/biome", "@swc/core", "esbuild", "protobufjs"],
|
||||
"dependencies": {
|
||||
"form-data": "^4.0.0",
|
||||
"idb": "^8.0.0",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { A, useSearchParams } from '@solidjs/router'
|
|||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||
import { isServer } from 'solid-js/web'
|
||||
import usePopper from 'solid-popper'
|
||||
|
||||
import { useFeed } from '~/context/feed'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
|
@ -13,7 +14,6 @@ import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
|
|||
import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||
import { processPrepositions } from '~/intl/prepositions'
|
||||
import { isCyrillic } from '~/intl/translate'
|
||||
import { createTooltip } from '~/lib/createTooltip'
|
||||
import { getImageUrl } from '~/lib/getThumbUrl'
|
||||
import { MediaItem } from '~/types/mediaitem'
|
||||
import { capitalize } from '~/utils/capitalize'
|
||||
|
@ -217,25 +217,25 @@ export const FullArticle = (props: Props) => {
|
|||
element.setAttribute('href', 'javascript: void(0)')
|
||||
}
|
||||
|
||||
const popperInstance = createTooltip(element, tooltip, {
|
||||
placement: 'top',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'eventListeners',
|
||||
options: { scroll: false }
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
const popperInstance = usePopper(
|
||||
() => element,
|
||||
() => tooltip,
|
||||
{
|
||||
placement: 'top',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: { fallbackPlacements: ['top'] }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: { fallbackPlacements: ['top'] }
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tooltip.style.visibility = 'hidden'
|
||||
let isTooltipVisible = false
|
||||
|
@ -248,7 +248,7 @@ export const FullArticle = (props: Props) => {
|
|||
isTooltipVisible = true
|
||||
}
|
||||
|
||||
popperInstance.update()
|
||||
popperInstance()?.update()
|
||||
}
|
||||
|
||||
const handleDocumentClick = (e: MouseEvent) => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import { clsx } from 'clsx'
|
||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js'
|
||||
import { Match, Show, Switch, createEffect, createSignal, 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 { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { MiniEditor } from '../MiniEditor'
|
||||
import { InsertLinkForm } from './InsertLinkForm'
|
||||
|
||||
|
@ -20,445 +20,381 @@ type BubbleMenuProps = {
|
|||
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||
const { t } = useLocalize()
|
||||
|
||||
const isActive = createMemo(
|
||||
() => (name: string, attributes?: Record<string, string | number>) =>
|
||||
props.editor?.isActive(name, attributes)
|
||||
)
|
||||
const isActive = (name: string, attributes?: Record<string, unknown>) =>
|
||||
createEditorTransaction(
|
||||
() => props.editor,
|
||||
(e) => e?.isActive(name, attributes)
|
||||
)
|
||||
|
||||
const [menuState, setMenuState] = createSignal({
|
||||
textSizeBubbleOpen: false,
|
||||
listBubbleOpen: false,
|
||||
linkEditorOpen: false,
|
||||
footnoteEditorOpen: false,
|
||||
footNote: undefined as string | undefined
|
||||
})
|
||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
||||
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
|
||||
const [footNote, setFootNote] = createSignal<string>()
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.shouldShow) {
|
||||
setMenuState((prev) => ({
|
||||
...prev,
|
||||
footNote: undefined,
|
||||
footnoteEditorOpen: false,
|
||||
linkEditorOpen: false,
|
||||
textSizeBubbleOpen: false,
|
||||
listBubbleOpen: false
|
||||
}))
|
||||
setFootNote()
|
||||
setFootnoteEditorOpen(false)
|
||||
}
|
||||
})
|
||||
|
||||
const activeStates = createMemo(() => ({
|
||||
bold: isActive()('bold'),
|
||||
italic: isActive()('italic'),
|
||||
h1: isActive()('heading', { level: 2 }),
|
||||
h2: isActive()('heading', { level: 3 }),
|
||||
h3: isActive()('heading', { level: 4 }),
|
||||
quote: isActive()('blockquote', { 'data-type': 'quote' }),
|
||||
punchLine: isActive()('blockquote', { 'data-type': 'punchline' }),
|
||||
orderedList: isActive()('orderedList'),
|
||||
bulletList: isActive()('bulletList'),
|
||||
link: isActive()('link'),
|
||||
highlight: isActive()('highlight'),
|
||||
footnote: isActive()('footnote'),
|
||||
incut: isActive()('article')
|
||||
// underline: isActive()('underline'),
|
||||
}))
|
||||
const isBold = isActive('bold')
|
||||
const isItalic = isActive('italic')
|
||||
const isH1 = isActive('heading', { level: 2 })
|
||||
const isH2 = isActive('heading', { level: 3 })
|
||||
const isH3 = isActive('heading', { level: 4 })
|
||||
const isQuote = isActive('blockquote', { 'data-type': 'quote' })
|
||||
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' })
|
||||
const isOrderedList = isActive('isOrderedList')
|
||||
const isBulletList = isActive('isBulletList')
|
||||
const isLink = isActive('link')
|
||||
const isHighlight = isActive('highlight')
|
||||
const isFootnote = isActive('footnote')
|
||||
const isIncut = isActive('article')
|
||||
|
||||
const togglePopup = (type: 'textSize' | 'list') => {
|
||||
setMenuState((prev) => ({
|
||||
...prev,
|
||||
textSizeBubbleOpen: type === 'textSize' ? !prev.textSizeBubbleOpen : false,
|
||||
listBubbleOpen: type === 'list' ? !prev.listBubbleOpen : false
|
||||
}))
|
||||
const toggleTextSizePopup = () => {
|
||||
if (listBubbleOpen()) {
|
||||
setListBubbleOpen(false)
|
||||
}
|
||||
setTextSizeBubbleOpen((prev) => !prev)
|
||||
}
|
||||
const toggleListPopup = () => {
|
||||
if (textSizeBubbleOpen()) {
|
||||
setTextSizeBubbleOpen(false)
|
||||
}
|
||||
setListBubbleOpen((prev) => !prev)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
|
||||
event.preventDefault()
|
||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
|
||||
setLinkEditorOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const updateCurrentFootnoteValue = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(ed) => {
|
||||
if (!activeStates().footnote) {
|
||||
if (!isFootnote()) {
|
||||
return
|
||||
}
|
||||
const value = ed.getAttributes('footnote').value
|
||||
setMenuState((prev) => ({ ...prev, footNote: value }))
|
||||
setFootNote(value)
|
||||
}
|
||||
)
|
||||
|
||||
const handleAddFootnote = (footnote: string) => {
|
||||
if (menuState().footNote) {
|
||||
props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
|
||||
const handleAddFootnote = (value: string) => {
|
||||
if (footNote()) {
|
||||
props.editor.chain().focus().updateFootnote({ value }).run()
|
||||
} else {
|
||||
props.editor?.chain().focus().setFootnote({ value: footnote }).run()
|
||||
props.editor.chain().focus().setFootnote({ value }).run()
|
||||
}
|
||||
setMenuState((prev) => ({
|
||||
...prev,
|
||||
footNote: undefined,
|
||||
linkEditorOpen: false,
|
||||
footnoteEditorOpen: false
|
||||
}))
|
||||
setFootNote()
|
||||
setFootnoteEditorOpen(false)
|
||||
}
|
||||
|
||||
const handleOpenFootnoteEditor = () => {
|
||||
updateCurrentFootnoteValue()
|
||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: false, footnoteEditorOpen: true }))
|
||||
setFootnoteEditorOpen(true)
|
||||
}
|
||||
|
||||
const handleSetPunchline = () => {
|
||||
if (activeStates().punchLine) {
|
||||
props.editor?.chain().focus().toggleBlockquote('punchline').run()
|
||||
if (isPunchLine()) {
|
||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||
}
|
||||
props.editor?.chain().focus().toggleBlockquote('quote').run()
|
||||
togglePopup('textSize')
|
||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||
toggleTextSizePopup()
|
||||
}
|
||||
const handleSetQuote = () => {
|
||||
if (activeStates().quote) {
|
||||
props.editor?.chain().focus().toggleBlockquote('quote').run()
|
||||
if (isQuote()) {
|
||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||
}
|
||||
props.editor?.chain().focus().toggleBlockquote('punchline').run()
|
||||
togglePopup('textSize')
|
||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||
toggleTextSizePopup()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
|
||||
})
|
||||
})
|
||||
|
||||
const handleOpenLinkForm = () => {
|
||||
props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
|
||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
|
||||
}
|
||||
|
||||
const handleCloseLinkForm = () => {
|
||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
|
||||
props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
||||
}
|
||||
const handleFormat = (type: 'Bold' | 'Italic' | 'Underline', _attributes?: Record<string, unknown>) => {
|
||||
props.editor?.chain().focus()[`toggle${type}`]().run()
|
||||
}
|
||||
|
||||
const ListBubbleMenu = (props: BubbleMenuProps) => {
|
||||
return (
|
||||
<div class={styles.dropDown}>
|
||||
<header>{t('Lists')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Bullet list')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().bulletList
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleBulletList().run()
|
||||
togglePopup('list')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-ul" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Ordered list')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().orderedList
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleOrderedList().run()
|
||||
togglePopup('list')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-ol" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CommonMarkupBubbleMenu = (props: BubbleMenuProps) => {
|
||||
return (
|
||||
<>
|
||||
<Popover content={t('Insert footnote')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().footnote
|
||||
})}
|
||||
onClick={handleOpenFootnoteEditor}
|
||||
>
|
||||
<Icon name="editor-footnote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<div class={styles.delimiter} />
|
||||
<div class={styles.dropDownHolder}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: menuState().listBubbleOpen
|
||||
})}
|
||||
onClick={() => togglePopup('list')}
|
||||
>
|
||||
<Icon name="editor-ul" />
|
||||
<Icon name="down-triangle" class={styles.triangle} />
|
||||
</button>
|
||||
<Show when={menuState().listBubbleOpen}>
|
||||
<ListBubbleMenu {...props} />
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TextSizeBubbleMenu = (props: BubbleMenuProps) => {
|
||||
return (
|
||||
<div class={styles.dropDown}>
|
||||
<header>{t('Headers')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Header 1')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().h1
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
togglePopup('textSize')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h1" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Header 2')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().h2
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
togglePopup('textSize')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h2" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Header 3')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().h3
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleHeading({ level: 4 }).run()
|
||||
togglePopup('textSize')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h3" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
<header>{t('Quotes')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Quote')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().quote
|
||||
})}
|
||||
onClick={handleSetPunchline}
|
||||
>
|
||||
<Icon name="editor-blockquote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Punchline')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().punchLine
|
||||
})}
|
||||
onClick={handleSetQuote}
|
||||
>
|
||||
<Icon name="editor-quote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
<header>{t('squib')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Incut')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().incut
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor?.chain().focus().toggleArticle().run()
|
||||
togglePopup('textSize')
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-squib" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BaseTextBubbleMenu = (props: BubbleMenuProps) => {
|
||||
return (
|
||||
<>
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<>
|
||||
<div class={styles.dropDownHolder}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: menuState().textSizeBubbleOpen
|
||||
})}
|
||||
onClick={() => togglePopup('textSize')}
|
||||
>
|
||||
<Icon name="editor-text-size" />
|
||||
<Icon name="down-triangle" class={styles.triangle} />
|
||||
</button>
|
||||
<Show when={menuState().textSizeBubbleOpen}>
|
||||
<TextSizeBubbleMenu {...props} />
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.delimiter} />
|
||||
</>
|
||||
</Show>
|
||||
<Popover content={t('Bold')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().bold
|
||||
})}
|
||||
onClick={() => handleFormat('Bold')}
|
||||
>
|
||||
<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]: activeStates().italic
|
||||
})}
|
||||
onClick={() => handleFormat('Italic')}
|
||||
>
|
||||
<Icon name="editor-italic" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
{/*<Popover content={t('Underline')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().underline
|
||||
})}
|
||||
onClick={() => handleFormat('Underline')}
|
||||
>
|
||||
<Icon name="editor-underline" />
|
||||
</button>
|
||||
)}
|
||||
</Popover> */}
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<Popover content={t('Highlight')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: activeStates().highlight
|
||||
})}
|
||||
onClick={() => props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
|
||||
>
|
||||
<div class={styles.toggleHighlight} />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<div class={styles.delimiter} />
|
||||
</Show>
|
||||
<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]: activeStates().link
|
||||
})}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<CommonMarkupBubbleMenu {...props} />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.ref}
|
||||
class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: menuState().footnoteEditorOpen })}
|
||||
>
|
||||
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
|
||||
<Switch>
|
||||
<Match when={menuState().linkEditorOpen}>
|
||||
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
|
||||
<Match when={linkEditorOpen()}>
|
||||
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
||||
</Match>
|
||||
<Match when={menuState().footnoteEditorOpen}>
|
||||
<Match when={footnoteEditorOpen()}>
|
||||
<MiniEditor
|
||||
placeholder={t('Enter footnote text')}
|
||||
onSubmit={handleAddFootnote}
|
||||
content={menuState().footNote}
|
||||
onCancel={() => setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))}
|
||||
onSubmit={(value) => handleAddFootnote(value)}
|
||||
content={footNote()}
|
||||
onCancel={() => setFootnoteEditorOpen(false)}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!(menuState().linkEditorOpen || menuState().footnoteEditorOpen)}>
|
||||
<BaseTextBubbleMenu {...props} />
|
||||
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
|
||||
<>
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<>
|
||||
<div class={styles.dropDownHolder}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
|
||||
})}
|
||||
onClick={toggleTextSizePopup}
|
||||
>
|
||||
<Icon name="editor-text-size" />
|
||||
<Icon name="down-triangle" class={styles.triangle} />
|
||||
</button>
|
||||
<Show when={textSizeBubbleOpen()}>
|
||||
<div class={styles.dropDown}>
|
||||
<header>{t('Headers')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Header 1')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH1()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h1" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Header 2')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH2()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h2" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Header 3')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isH3()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleHeading({ level: 4 }).run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-h3" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
<header>{t('Quotes')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Quote')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isQuote()
|
||||
})}
|
||||
onClick={handleSetPunchline}
|
||||
>
|
||||
<Icon name="editor-blockquote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Punchline')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isPunchLine()
|
||||
})}
|
||||
onClick={handleSetQuote}
|
||||
>
|
||||
<Icon name="editor-quote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
<header>{t('squib')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Incut')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isIncut()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleArticle().run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-squib" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.delimiter} />
|
||||
</>
|
||||
</Show>
|
||||
<Popover content={t('Bold')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => 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: HTMLButtonElement) => 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>
|
||||
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<Popover content={t('Highlight')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isHighlight()
|
||||
})}
|
||||
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
|
||||
>
|
||||
<div class={styles.toggleHighlight} />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<div class={styles.delimiter} />
|
||||
</Show>
|
||||
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={() => setLinkEditorOpen(true)}
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isLink()
|
||||
})}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Show when={!props.isCommonMarkup}>
|
||||
<>
|
||||
<Popover content={t('Insert footnote')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isFootnote()
|
||||
})}
|
||||
onClick={handleOpenFootnoteEditor}
|
||||
>
|
||||
<Icon name="editor-footnote" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<div class={styles.delimiter} />
|
||||
<div class={styles.dropDownHolder}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: listBubbleOpen()
|
||||
})}
|
||||
onClick={toggleListPopup}
|
||||
>
|
||||
<Icon name="editor-ul" />
|
||||
<Icon name="down-triangle" class={styles.triangle} />
|
||||
</button>
|
||||
<Show when={listBubbleOpen()}>
|
||||
<div class={styles.dropDown}>
|
||||
<header>{t('Lists')}</header>
|
||||
<div class={styles.actions}>
|
||||
<Popover content={t('Bullet list')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBulletList()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleBulletList().run()
|
||||
toggleListPopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-ul" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Ordered list')}>
|
||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isOrderedList()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleOrderedList().run()
|
||||
toggleListPopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-ol" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLElement, options = {}) {
|
||||
const defaultOptions = {
|
||||
placement: 'top',
|
||||
offset: [0, 8],
|
||||
flip: {
|
||||
fallbackPlacements: ['top', 'bottom']
|
||||
}
|
||||
}
|
||||
const config = { ...defaultOptions, ...options }
|
||||
|
||||
function updatePosition() {
|
||||
if (!(referenceElement && tooltipElement)) return
|
||||
|
||||
const rect = referenceElement.getBoundingClientRect()
|
||||
const tooltipRect = tooltipElement.getBoundingClientRect()
|
||||
const offsetX = config.offset[0]
|
||||
const offsetY = config.offset[1]
|
||||
|
||||
let placement = config.placement
|
||||
let top = 0
|
||||
let left = 0
|
||||
|
||||
// Базовое позиционирование
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
top = rect.top - tooltipRect.height - offsetY
|
||||
left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX
|
||||
break
|
||||
case 'bottom':
|
||||
top = rect.bottom + offsetY
|
||||
left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX
|
||||
break
|
||||
// Добавьте case для 'left' и 'right', если необходимо
|
||||
}
|
||||
|
||||
// Проверка на выход за границы окна и применение flip
|
||||
if (top < 0 && config.flip.fallbackPlacements.includes('bottom')) {
|
||||
top = rect.bottom + offsetY
|
||||
placement = 'bottom'
|
||||
} else if (top + tooltipRect.height > window.innerHeight && config.flip.fallbackPlacements.includes('top')) {
|
||||
top = rect.top - tooltipRect.height - offsetY
|
||||
placement = 'top'
|
||||
}
|
||||
|
||||
// Применение позиции
|
||||
tooltipElement.style.position = 'absolute'
|
||||
tooltipElement.style.top = `${top}px`
|
||||
tooltipElement.style.left = `${left}px`
|
||||
tooltipElement.style.visibility = 'visible'
|
||||
|
||||
// Обновление класса для стрелки
|
||||
tooltipElement.setAttribute('data-popper-placement', placement)
|
||||
|
||||
// Позиционирование стрелки
|
||||
const arrow = tooltipElement.querySelector('[data-popper-arrow]') as HTMLElement
|
||||
if (arrow) {
|
||||
const arrowRect = arrow.getBoundingClientRect()
|
||||
if (placement === 'top') {
|
||||
arrow.style.bottom = '-4px'
|
||||
arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px`
|
||||
} else if (placement === 'bottom') {
|
||||
arrow.style.top = '-4px'
|
||||
arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip() {
|
||||
if (tooltipElement) {
|
||||
tooltipElement.style.visibility = 'visible'
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
if (tooltipElement) tooltipElement.style.visibility = 'hidden'
|
||||
}
|
||||
|
||||
referenceElement?.addEventListener('mouseenter', showTooltip)
|
||||
referenceElement?.addEventListener('mouseleave', hideTooltip)
|
||||
window.addEventListener('resize', updatePosition)
|
||||
window.addEventListener('scroll', updatePosition)
|
||||
|
||||
return {
|
||||
update: updatePosition,
|
||||
destroy() {
|
||||
referenceElement?.removeEventListener('mouseenter', showTooltip)
|
||||
referenceElement?.removeEventListener('mouseleave', hideTooltip)
|
||||
window.removeEventListener('resize', updatePosition)
|
||||
window.removeEventListener('scroll', updatePosition)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user