popper-fix+textbubble-revert
This commit is contained in:
parent
cd39b7b806
commit
d28b4f2ab3
|
@ -133,12 +133,7 @@
|
||||||
"yjs": "13.6.19",
|
"yjs": "13.6.19",
|
||||||
"y-prosemirror": "1.2.12"
|
"y-prosemirror": "1.2.12"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": ["@biomejs/biome", "@swc/core", "esbuild", "protobufjs"],
|
||||||
"@biomejs/biome",
|
|
||||||
"@swc/core",
|
|
||||||
"esbuild",
|
|
||||||
"protobufjs"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { A, useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { isServer } from 'solid-js/web'
|
import { isServer } from 'solid-js/web'
|
||||||
|
import usePopper from 'solid-popper'
|
||||||
|
|
||||||
import { useFeed } from '~/context/feed'
|
import { useFeed } from '~/context/feed'
|
||||||
import { useLocalize } from '~/context/localize'
|
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 type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import { processPrepositions } from '~/intl/prepositions'
|
import { processPrepositions } from '~/intl/prepositions'
|
||||||
import { isCyrillic } from '~/intl/translate'
|
import { isCyrillic } from '~/intl/translate'
|
||||||
import { createTooltip } from '~/lib/createTooltip'
|
|
||||||
import { getImageUrl } from '~/lib/getThumbUrl'
|
import { getImageUrl } from '~/lib/getThumbUrl'
|
||||||
import { MediaItem } from '~/types/mediaitem'
|
import { MediaItem } from '~/types/mediaitem'
|
||||||
import { capitalize } from '~/utils/capitalize'
|
import { capitalize } from '~/utils/capitalize'
|
||||||
|
@ -217,25 +217,25 @@ export const FullArticle = (props: Props) => {
|
||||||
element.setAttribute('href', 'javascript: void(0)')
|
element.setAttribute('href', 'javascript: void(0)')
|
||||||
}
|
}
|
||||||
|
|
||||||
const popperInstance = createTooltip(element, tooltip, {
|
const popperInstance = usePopper(
|
||||||
placement: 'top',
|
() => element,
|
||||||
modifiers: [
|
() => tooltip,
|
||||||
{
|
{
|
||||||
name: 'eventListeners',
|
placement: 'top',
|
||||||
options: { scroll: false }
|
modifiers: [
|
||||||
},
|
{
|
||||||
{
|
name: 'offset',
|
||||||
name: 'offset',
|
options: {
|
||||||
options: {
|
offset: [0, 8]
|
||||||
offset: [0, 8]
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
options: { fallbackPlacements: ['top'] }
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
}
|
||||||
name: 'flip',
|
)
|
||||||
options: { fallbackPlacements: ['top'] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tooltip.style.visibility = 'hidden'
|
tooltip.style.visibility = 'hidden'
|
||||||
let isTooltipVisible = false
|
let isTooltipVisible = false
|
||||||
|
@ -248,7 +248,7 @@ export const FullArticle = (props: Props) => {
|
||||||
isTooltipVisible = true
|
isTooltipVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
popperInstance.update()
|
popperInstance()?.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDocumentClick = (e: MouseEvent) => {
|
const handleDocumentClick = (e: MouseEvent) => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
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, createMemo, createSignal, onCleanup, onMount } from 'solid-js'
|
import { Match, Show, Switch, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
import { createEditorTransaction } from 'solid-tiptap'
|
import { createEditorTransaction } from 'solid-tiptap'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Popover } from '~/components/_shared/Popover'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { Popover } from '../../_shared/Popover'
|
||||||
import { MiniEditor } from '../MiniEditor'
|
import { MiniEditor } from '../MiniEditor'
|
||||||
import { InsertLinkForm } from './InsertLinkForm'
|
import { InsertLinkForm } from './InsertLinkForm'
|
||||||
|
|
||||||
|
@ -20,445 +20,381 @@ type BubbleMenuProps = {
|
||||||
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const isActive = createMemo(
|
const isActive = (name: string, attributes?: Record<string, unknown>) =>
|
||||||
() => (name: string, attributes?: Record<string, string | number>) =>
|
createEditorTransaction(
|
||||||
props.editor?.isActive(name, attributes)
|
() => props.editor,
|
||||||
)
|
(e) => e?.isActive(name, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
const [menuState, setMenuState] = createSignal({
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||||
textSizeBubbleOpen: false,
|
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
||||||
listBubbleOpen: false,
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
||||||
linkEditorOpen: false,
|
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
|
||||||
footnoteEditorOpen: false,
|
const [footNote, setFootNote] = createSignal<string>()
|
||||||
footNote: undefined as string | undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!props.shouldShow) {
|
if (!props.shouldShow) {
|
||||||
setMenuState((prev) => ({
|
setFootNote()
|
||||||
...prev,
|
setFootnoteEditorOpen(false)
|
||||||
footNote: undefined,
|
|
||||||
footnoteEditorOpen: false,
|
|
||||||
linkEditorOpen: false,
|
|
||||||
textSizeBubbleOpen: false,
|
|
||||||
listBubbleOpen: false
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeStates = createMemo(() => ({
|
const isBold = isActive('bold')
|
||||||
bold: isActive()('bold'),
|
const isItalic = isActive('italic')
|
||||||
italic: isActive()('italic'),
|
const isH1 = isActive('heading', { level: 2 })
|
||||||
h1: isActive()('heading', { level: 2 }),
|
const isH2 = isActive('heading', { level: 3 })
|
||||||
h2: isActive()('heading', { level: 3 }),
|
const isH3 = isActive('heading', { level: 4 })
|
||||||
h3: isActive()('heading', { level: 4 }),
|
const isQuote = isActive('blockquote', { 'data-type': 'quote' })
|
||||||
quote: isActive()('blockquote', { 'data-type': 'quote' }),
|
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' })
|
||||||
punchLine: isActive()('blockquote', { 'data-type': 'punchline' }),
|
const isOrderedList = isActive('isOrderedList')
|
||||||
orderedList: isActive()('orderedList'),
|
const isBulletList = isActive('isBulletList')
|
||||||
bulletList: isActive()('bulletList'),
|
const isLink = isActive('link')
|
||||||
link: isActive()('link'),
|
const isHighlight = isActive('highlight')
|
||||||
highlight: isActive()('highlight'),
|
const isFootnote = isActive('footnote')
|
||||||
footnote: isActive()('footnote'),
|
const isIncut = isActive('article')
|
||||||
incut: isActive()('article')
|
|
||||||
// underline: isActive()('underline'),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const togglePopup = (type: 'textSize' | 'list') => {
|
const toggleTextSizePopup = () => {
|
||||||
setMenuState((prev) => ({
|
if (listBubbleOpen()) {
|
||||||
...prev,
|
setListBubbleOpen(false)
|
||||||
textSizeBubbleOpen: type === 'textSize' ? !prev.textSizeBubbleOpen : false,
|
}
|
||||||
listBubbleOpen: type === 'list' ? !prev.listBubbleOpen : false
|
setTextSizeBubbleOpen((prev) => !prev)
|
||||||
}))
|
}
|
||||||
|
const toggleListPopup = () => {
|
||||||
|
if (textSizeBubbleOpen()) {
|
||||||
|
setTextSizeBubbleOpen(false)
|
||||||
|
}
|
||||||
|
setListBubbleOpen((prev) => !prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
|
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
|
setLinkEditorOpen(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCurrentFootnoteValue = createEditorTransaction(
|
const updateCurrentFootnoteValue = createEditorTransaction(
|
||||||
() => props.editor,
|
() => props.editor,
|
||||||
(ed) => {
|
(ed) => {
|
||||||
if (!activeStates().footnote) {
|
if (!isFootnote()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const value = ed.getAttributes('footnote').value
|
const value = ed.getAttributes('footnote').value
|
||||||
setMenuState((prev) => ({ ...prev, footNote: value }))
|
setFootNote(value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleAddFootnote = (footnote: string) => {
|
const handleAddFootnote = (value: string) => {
|
||||||
if (menuState().footNote) {
|
if (footNote()) {
|
||||||
props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
|
props.editor.chain().focus().updateFootnote({ value }).run()
|
||||||
} else {
|
} else {
|
||||||
props.editor?.chain().focus().setFootnote({ value: footnote }).run()
|
props.editor.chain().focus().setFootnote({ value }).run()
|
||||||
}
|
}
|
||||||
setMenuState((prev) => ({
|
setFootNote()
|
||||||
...prev,
|
setFootnoteEditorOpen(false)
|
||||||
footNote: undefined,
|
|
||||||
linkEditorOpen: false,
|
|
||||||
footnoteEditorOpen: false
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenFootnoteEditor = () => {
|
const handleOpenFootnoteEditor = () => {
|
||||||
updateCurrentFootnoteValue()
|
updateCurrentFootnoteValue()
|
||||||
setMenuState((prev) => ({ ...prev, linkEditorOpen: false, footnoteEditorOpen: true }))
|
setFootnoteEditorOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSetPunchline = () => {
|
const handleSetPunchline = () => {
|
||||||
if (activeStates().punchLine) {
|
if (isPunchLine()) {
|
||||||
props.editor?.chain().focus().toggleBlockquote('punchline').run()
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||||
}
|
}
|
||||||
props.editor?.chain().focus().toggleBlockquote('quote').run()
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||||
togglePopup('textSize')
|
toggleTextSizePopup()
|
||||||
}
|
}
|
||||||
const handleSetQuote = () => {
|
const handleSetQuote = () => {
|
||||||
if (activeStates().quote) {
|
if (isQuote()) {
|
||||||
props.editor?.chain().focus().toggleBlockquote('quote').run()
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||||
}
|
}
|
||||||
props.editor?.chain().focus().toggleBlockquote('punchline').run()
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||||
togglePopup('textSize')
|
toggleTextSizePopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
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 (
|
return (
|
||||||
<div
|
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
|
||||||
ref={props.ref}
|
|
||||||
class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: menuState().footnoteEditorOpen })}
|
|
||||||
>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={menuState().linkEditorOpen}>
|
<Match when={linkEditorOpen()}>
|
||||||
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
|
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={menuState().footnoteEditorOpen}>
|
<Match when={footnoteEditorOpen()}>
|
||||||
<MiniEditor
|
<MiniEditor
|
||||||
placeholder={t('Enter footnote text')}
|
placeholder={t('Enter footnote text')}
|
||||||
onSubmit={handleAddFootnote}
|
onSubmit={(value) => handleAddFootnote(value)}
|
||||||
content={menuState().footNote}
|
content={footNote()}
|
||||||
onCancel={() => setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))}
|
onCancel={() => setFootnoteEditorOpen(false)}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!(menuState().linkEditorOpen || menuState().footnoteEditorOpen)}>
|
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
|
||||||
<BaseTextBubbleMenu {...props} />
|
<>
|
||||||
|
<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>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</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