Files
Compass/web/components/editor/sticky-format-menu.tsx
2026-03-01 16:55:19 +01:00

119 lines
3.5 KiB
TypeScript

import {FaceSmileIcon} from '@heroicons/react/24/outline'
import {CodeBracketIcon, PhotoIcon} from '@heroicons/react/24/solid'
import {Editor} from '@tiptap/react'
import {MouseEventHandler, useState} from 'react'
import {Row} from 'web/components/layout/row'
import {useT} from 'web/lib/locale'
import {FileUploadButton} from '../buttons/file-upload-button'
import {LoadingIndicator} from '../widgets/loading-indicator'
import {Tooltip} from '../widgets/tooltip'
import {EmbedModal} from './embed-modal'
import type {UploadMutation} from './upload-extension'
/* Toolbar, with buttons for images and embeds */
export function StickyFormatMenu(props: {
editor: Editor | null
hideEmbed?: boolean
children?: React.ReactNode
}) {
const {editor, hideEmbed, children} = props
const upload = editor?.storage.upload.mutation
const t = useT()
const [iframeOpen, setIframeOpen] = useState(false)
return (
<Row className="text-ink-600 h-8 items-center">
<UploadButton key={'upload-button'} upload={upload} />
{!hideEmbed && (
<ToolbarButton
key={'embed-button'}
label={t('sticky_format_menu.add_embed', 'Add embed')}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
setIframeOpen(true)
}}
>
<CodeBracketIcon className="h-5 w-5" aria-hidden="true" />
</ToolbarButton>
)}
<ToolbarButton
key={'emoji-button'}
label={t('sticky_format_menu.add_emoji', 'Add emoji')}
onClick={() => insertEmoji(editor)}
>
<FaceSmileIcon className="h-5 w-5" />
</ToolbarButton>
<EmbedModal editor={editor} open={iframeOpen} setOpen={setIframeOpen} />
<div className="grow" />
{children}
</Row>
)
}
function UploadButton(props: {upload: UploadMutation}) {
const {upload} = props
const t = useT()
return (
<Tooltip
text={t('sticky_format_menu.upload_image', 'Upload image')}
className="flex items-stretch"
placement="bottom"
>
<FileUploadButton
onFiles={(files) => upload?.mutate(files)}
className="hover:text-ink-700 disabled:text-ink-300 active:text-ink-800 text-ink-400 relative flex rounded px-3 py-1 pl-4 transition-colors"
>
<Row className={'items-center justify-start gap-2'}>
<PhotoIcon className="h-5 w-5" aria-hidden="true" />
{upload?.isLoading && (
<LoadingIndicator
className="absolute bottom-0 left-0 right-0 top-0"
spinnerClassName="!h-6 !w-6 !border-2"
/>
)}
</Row>
</FileUploadButton>
</Tooltip>
)
}
function ToolbarButton(props: {
label: string
onClick: MouseEventHandler
children: React.ReactNode
}) {
const {label, onClick, children} = props
return (
<Tooltip text={label} className="flex items-stretch" placement="bottom">
<button
type="button"
onClick={onClick}
className="text-ink-400 hover:text-ink-700 active:text-ink-800 disabled:text-ink-300 flex rounded px-3 py-1 transition-colors"
>
{children}
</button>
</Tooltip>
)
}
/** insert a colon, and a space if necessary, to bring up emoji selector */
const insertEmoji = (editor: Editor | null) => {
if (!editor) return
const textBefore = editor.view.state.selection.$from.nodeBefore?.text
const addSpace = textBefore && !textBefore.endsWith(' ')
editor
.chain()
.focus()
.createParagraphNear()
.insertContent(addSpace ? ' :' : ':')
.run()
}