webapp/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx

424 lines
16 KiB
TypeScript
Raw Normal View History

2023-05-04 12:16:39 +00:00
import type { Editor } from '@tiptap/core'
import { clsx } from 'clsx'
2024-02-04 11:25:21 +00:00
import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
2023-05-04 12:16:39 +00:00
import { createEditorTransaction } from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon'
import { Popover } from '~/components/_shared/Popover'
import { useLocalize } from '~/context/localize'
2024-09-27 17:57:25 +00:00
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
2023-05-04 12:16:39 +00:00
2024-09-30 11:00:02 +00:00
import styles from './TextBubbleMenu.module.scss'
2024-09-27 18:09:50 +00:00
const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor'))
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
shouldShow: boolean
2023-05-04 12:16:39 +00:00
}
export const TextBubbleMenu = (props: BubbleMenuProps) => {
const { t } = useLocalize()
2024-06-24 17:50:27 +00:00
const isActive = (name: string, attributes?: Record<string, string | number>) =>
2023-05-04 12:16:39 +00:00
createEditorTransaction(
() => props.editor,
2024-06-26 08:22:05 +00:00
(editor) => editor?.isActive(name, attributes)
2023-05-04 12:16:39 +00:00
)
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>()
2023-05-04 12:16:39 +00:00
createEffect(() => {
if (!props.shouldShow) {
setFootNote()
setFootnoteEditorOpen(false)
2024-01-17 15:33:25 +00:00
setLinkEditorOpen(false)
setTextSizeBubbleOpen(false)
setListBubbleOpen(false)
}
})
2023-05-04 12:16:39 +00:00
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' })
2023-05-04 12:16:39 +00:00
const isOrderedList = isActive('isOrderedList')
const isBulletList = isActive('isBulletList')
const isLink = isActive('link')
2023-05-09 17:31:28 +00:00
const isHighlight = isActive('highlight')
const isFootnote = isActive('footnote')
const isIncut = isActive('article')
2023-05-04 12:16:39 +00:00
const toggleTextSizePopup = () => {
if (listBubbleOpen()) {
setListBubbleOpen(false)
}
setTextSizeBubbleOpen((prev) => !prev)
}
const toggleListPopup = () => {
if (textSizeBubbleOpen()) {
setTextSizeBubbleOpen(false)
}
setListBubbleOpen((prev) => !prev)
}
2024-06-24 17:50:27 +00:00
const handleKeyDown = (event: KeyboardEvent) => {
2023-07-28 19:53:21 +00:00
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
event.preventDefault()
setLinkEditorOpen(true)
}
}
const updateCurrentFootnoteValue = createEditorTransaction(
() => props.editor,
(ed) => {
if (!isFootnote()) {
return
}
const value = ed.getAttributes('footnote').value
setFootNote(value)
2024-06-26 08:22:05 +00:00
}
)
2024-06-24 17:50:27 +00:00
const handleAddFootnote = (footnote: string) => {
if (footNote()) {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
} else {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().setFootnote({ value: footnote }).run()
}
setFootNote()
2024-01-17 15:33:25 +00:00
setLinkEditorOpen(false)
setFootnoteEditorOpen(false)
}
const handleOpenFootnoteEditor = () => {
updateCurrentFootnoteValue()
2024-01-17 15:33:25 +00:00
setLinkEditorOpen(false)
setFootnoteEditorOpen(true)
}
2023-09-05 07:59:36 +00:00
const handleSetPunchline = () => {
if (isPunchLine()) {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleBlockquote('punchline').run()
2023-09-05 07:59:36 +00:00
}
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleBlockquote('quote').run()
2023-09-05 07:59:36 +00:00
toggleTextSizePopup()
}
const handleSetQuote = () => {
if (isQuote()) {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleBlockquote('quote').run()
2023-09-05 07:59:36 +00:00
}
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleBlockquote('punchline').run()
2023-09-05 07:59:36 +00:00
toggleTextSizePopup()
}
2023-07-28 19:53:21 +00:00
onMount(() => {
window.addEventListener('keydown', handleKeyDown)
onCleanup(() => {
window.removeEventListener('keydown', handleKeyDown)
2024-01-17 15:33:25 +00:00
setLinkEditorOpen(false)
})
2023-07-28 19:53:21 +00:00
})
2023-05-04 12:16:39 +00:00
const handleOpenLinkForm = () => {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
setLinkEditorOpen(true)
}
const handleCloseLinkForm = () => {
setLinkEditorOpen(false)
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
}
2023-05-04 12:16:39 +00:00
return (
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
2023-05-04 12:16:39 +00:00
<Switch>
<Match when={linkEditorOpen()}>
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
2023-05-04 12:16:39 +00:00
</Match>
<Match when={footnoteEditorOpen()}>
2024-09-27 16:31:54 +00:00
<MiniEditor
placeholder={t('Enter footnote text')}
2024-09-27 16:31:54 +00:00
onSubmit={(value: string) => handleAddFootnote(value)}
content={footNote()}
onCancel={() => {
setFootnoteEditorOpen(false)
}}
/>
</Match>
2024-02-04 17:40:15 +00:00
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
2023-05-04 12:16:39 +00:00
<>
2023-05-11 11:43:14 +00:00
<Show when={!props.isCommonMarkup}>
<>
<div class={styles.dropDownHolder}>
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
2023-05-11 11:43:14 +00:00
})}
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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isH1()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isH2()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isH3()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isQuote()
2023-05-29 17:14:58 +00:00
})}
2023-09-05 07:59:36 +00:00
onClick={handleSetPunchline}
2023-05-29 17:14:58 +00:00
>
<Icon name="editor-blockquote" />
</button>
)}
</Popover>
<Popover content={t('Punchline')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isPunchLine()
2023-05-29 17:14:58 +00:00
})}
2023-09-05 07:59:36 +00:00
onClick={handleSetQuote}
2023-05-29 17:14:58 +00:00
>
<Icon name="editor-quote" />
</button>
)}
</Popover>
2023-05-11 11:43:14 +00:00
</div>
<header>{t('squib')}</header>
<div class={styles.actions}>
2023-05-29 17:14:58 +00:00
<Popover content={t('Incut')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isIncut()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleArticle().run()
2023-05-29 17:14:58 +00:00
toggleTextSizePopup()
}}
>
<Icon name="editor-squib" />
</button>
)}
</Popover>
</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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isBold()
2023-05-29 17:14:58 +00:00
})}
2024-09-15 18:43:35 +00:00
onClick={() => props.editor?.chain().focus().toggleBold().run()}
2023-05-29 17:14:58 +00:00
>
<Icon name="editor-bold" />
</button>
)}
</Popover>
<Popover content={t('Italic')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isItalic()
2023-05-29 17:14:58 +00:00
})}
2024-09-15 18:43:35 +00:00
onClick={() => props.editor?.chain().focus().toggleItalic().run()}
2023-05-29 17:14:58 +00:00
>
<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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isHighlight()
2023-05-29 17:14:58 +00:00
})}
2024-09-15 18:43:35 +00:00
onClick={() =>
props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()
}
2023-05-29 17:14:58 +00:00
>
<div class={styles.toggleHighlight} />
</button>
)}
</Popover>
<div class={styles.delimiter} />
2023-05-11 11:43:14 +00:00
</Show>
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
onClick={handleOpenLinkForm}
2023-05-29 17:14:58 +00:00
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isLink()
2023-05-29 17:14:58 +00:00
})}
>
<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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isFootnote()
})}
onClick={handleOpenFootnoteEditor}
>
2023-05-29 17:14:58 +00:00
<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, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: listBubbleOpen()
2023-05-11 11:43:14 +00:00
})}
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')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isBulletList()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleBulletList().run()
2023-05-29 17:14:58 +00:00
toggleListPopup()
}}
>
<Icon name="editor-ul" />
</button>
)}
</Popover>
<Popover content={t('Ordered list')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
2023-05-29 17:14:58 +00:00
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
2024-06-26 08:22:05 +00:00
[styles.bubbleMenuButtonActive]: isOrderedList()
2023-05-29 17:14:58 +00:00
})}
onClick={() => {
2024-09-15 18:43:35 +00:00
props.editor?.chain().focus().toggleOrderedList().run()
2023-05-29 17:14:58 +00:00
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>
)
}