2023-05-04 12:16:39 +00:00
|
|
|
import { Switch, Match, createSignal, Show } from 'solid-js'
|
|
|
|
import type { Editor } from '@tiptap/core'
|
|
|
|
import styles from './TextBubbleMenu.module.scss'
|
|
|
|
import { Icon } from '../../_shared/Icon'
|
|
|
|
import { clsx } from 'clsx'
|
|
|
|
import { createEditorTransaction } from 'solid-tiptap'
|
|
|
|
import { useLocalize } from '../../../context/localize'
|
|
|
|
import { InlineForm } from '../InlineForm'
|
2023-05-09 23:15:26 +00:00
|
|
|
import { validateUrl } from '../../../utils/validateUrl'
|
2023-05-29 17:14:58 +00:00
|
|
|
import { Popover } from '../../_shared/Popover'
|
2023-05-04 12:16:39 +00:00
|
|
|
|
|
|
|
type BubbleMenuProps = {
|
|
|
|
editor: Editor
|
2023-05-11 11:43:14 +00:00
|
|
|
isCommonMarkup: boolean
|
2023-05-04 12:16:39 +00:00
|
|
|
ref: (el: HTMLDivElement) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|
|
|
const { t } = useLocalize()
|
|
|
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal<boolean>(false)
|
|
|
|
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
|
|
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
|
|
|
|
|
|
|
const isActive = (name: string, attributes?: unknown) =>
|
|
|
|
createEditorTransaction(
|
|
|
|
() => props.editor,
|
|
|
|
(editor) => {
|
|
|
|
return editor && editor.isActive(name, attributes)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const isBold = isActive('bold')
|
|
|
|
const isItalic = isActive('italic')
|
2023-06-24 14:45:57 +00:00
|
|
|
const isH1 = isActive('heading', { level: 2 })
|
|
|
|
const isH2 = isActive('heading', { level: 3 })
|
|
|
|
const isH3 = isActive('heading', { level: 4 })
|
2023-05-04 12:16:39 +00:00
|
|
|
const isBlockQuote = isActive('blockquote')
|
|
|
|
const isOrderedList = isActive('isOrderedList')
|
|
|
|
const isBulletList = isActive('isBulletList')
|
|
|
|
const isLink = isActive('link')
|
2023-05-09 17:31:28 +00:00
|
|
|
const isHighlight = isActive('highlight')
|
2023-05-04 12:16:39 +00:00
|
|
|
|
|
|
|
const toggleLinkForm = () => {
|
|
|
|
setLinkEditorOpen(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
const toggleTextSizePopup = () => {
|
|
|
|
if (listBubbleOpen()) {
|
|
|
|
setListBubbleOpen(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
setTextSizeBubbleOpen((prev) => !prev)
|
|
|
|
}
|
|
|
|
const toggleListPopup = () => {
|
|
|
|
if (textSizeBubbleOpen()) {
|
|
|
|
setTextSizeBubbleOpen(false)
|
|
|
|
}
|
|
|
|
setListBubbleOpen((prev) => !prev)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleLinkFormSubmit = (value: string) => {
|
|
|
|
props.editor.chain().focus().setLink({ href: value }).run()
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentUrl = createEditorTransaction(
|
|
|
|
() => props.editor,
|
|
|
|
(editor) => {
|
|
|
|
return (editor && editor.getAttributes('link').href) || ''
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const handleClearLinkForm = () => {
|
|
|
|
if (currentUrl()) {
|
|
|
|
props.editor.chain().focus().unsetLink().run()
|
|
|
|
}
|
|
|
|
setLinkEditorOpen(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2023-05-04 13:36:53 +00:00
|
|
|
<div ref={props.ref} class={styles.TextBubbleMenu}>
|
2023-05-04 12:16:39 +00:00
|
|
|
<Switch>
|
|
|
|
<Match when={linkEditorOpen()}>
|
|
|
|
<InlineForm
|
|
|
|
placeholder={t('Enter URL address')}
|
|
|
|
initialValue={currentUrl() ?? ''}
|
|
|
|
onClear={handleClearLinkForm}
|
2023-05-09 23:15:26 +00:00
|
|
|
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
|
2023-05-04 12:16:39 +00:00
|
|
|
onSubmit={handleLinkFormSubmit}
|
|
|
|
onClose={() => setLinkEditorOpen(false)}
|
|
|
|
/>
|
|
|
|
</Match>
|
|
|
|
<Match when={!linkEditorOpen()}>
|
|
|
|
<>
|
2023-05-11 11:43:14 +00:00
|
|
|
<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}>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Header 1')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isH1()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
2023-06-24 14:45:57 +00:00
|
|
|
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
2023-05-29 17:14:58 +00:00
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-h1" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Popover content={t('Header 2')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isH2()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
2023-06-24 14:45:57 +00:00
|
|
|
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
2023-05-29 17:14:58 +00:00
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-h2" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Popover content={t('Header 3')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isH3()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
2023-06-24 14:45:57 +00:00
|
|
|
props.editor.chain().focus().toggleHeading({ level: 4 }).run()
|
2023-05-29 17:14:58 +00:00
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-h3" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-05-11 11:43:14 +00:00
|
|
|
</div>
|
|
|
|
<header>{t('Quotes')}</header>
|
|
|
|
<div class={styles.actions}>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Quote')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
|
|
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-blockquote" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
<Popover content={t('Punchline')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
|
|
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-quote" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-05-11 11:43:14 +00:00
|
|
|
</div>
|
2023-05-29 10:09:44 +00:00
|
|
|
<header>{t('squib')}</header>
|
|
|
|
<div class={styles.actions}>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Incut')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
|
|
|
})}
|
|
|
|
onClick={() => {
|
|
|
|
props.editor.chain().focus().toggleArticle().run()
|
|
|
|
toggleTextSizePopup()
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon name="editor-squib" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-05-29 10:09:44 +00:00
|
|
|
</div>
|
2023-05-11 11:43:14 +00:00
|
|
|
</div>
|
|
|
|
</Show>
|
2023-05-04 12:16:39 +00:00
|
|
|
</div>
|
2023-05-11 11:43:14 +00:00
|
|
|
<div class={styles.delimiter} />
|
|
|
|
</>
|
|
|
|
</Show>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Bold')}>
|
|
|
|
{(triggerRef: (el) => 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) => 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>
|
2023-05-11 11:43:14 +00:00
|
|
|
|
|
|
|
<Show when={!props.isCommonMarkup}>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Highlight')}>
|
|
|
|
{(triggerRef: (el) => 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>
|
2023-05-11 11:43:14 +00:00
|
|
|
</Show>
|
2023-05-04 12:16:39 +00:00
|
|
|
<div class={styles.delimiter} />
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Add url')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button
|
|
|
|
ref={triggerRef}
|
|
|
|
type="button"
|
|
|
|
onClick={toggleLinkForm}
|
|
|
|
class={clsx(styles.bubbleMenuButton, {
|
|
|
|
[styles.bubbleMenuButtonActive]: isLink()
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<Icon name="editor-link" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-05-11 11:43:14 +00:00
|
|
|
<Show when={!props.isCommonMarkup}>
|
|
|
|
<>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Insert footnote')}>
|
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<button ref={triggerRef} type="button" class={styles.bubbleMenuButton}>
|
|
|
|
<Icon name="editor-footnote" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-05-11 11:43:14 +00:00
|
|
|
<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}>
|
2023-05-29 17:14:58 +00:00
|
|
|
<Popover content={t('Bullet list')}>
|
|
|
|
{(triggerRef: (el) => 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) => 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>
|
2023-05-11 11:43:14 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Show>
|
2023-05-04 12:16:39 +00:00
|
|
|
</div>
|
2023-05-11 11:43:14 +00:00
|
|
|
</>
|
|
|
|
</Show>
|
2023-05-04 12:16:39 +00:00
|
|
|
</>
|
|
|
|
</Match>
|
|
|
|
</Switch>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|