Tom Butcher 8ac04ac0bc Implement Tooltip System and Enhance UI Components
- Introduced a new Tooltip component to standardize tooltip functionality across the application, improving consistency and usability.
- Refactored existing components to utilize the new Tooltip implementation, enhancing user interactions with clearer feedback.
- Added a CursorTooltip for dynamic tooltip positioning based on mouse movement, improving user experience.
- Updated CSS styles for tooltips and related components to enhance visual clarity and responsiveness.
- Integrated TooltipProvider to manage tooltip state and visibility, streamlining tooltip management across the application.
2026-08-20 20:14:51 +01:00

348 lines
9.3 KiB
JavaScript

import { MinusOutlined } from '@ant-design/icons'
import { useEditorState } from '@tiptap/react'
import { useState } from 'react'
import {
Button,
Card,
Checkbox,
Flex,
Input,
Popover,
Select,
Space
} from 'antd'
import PropTypes from 'prop-types'
import Tooltip from './Tooltip'
import BlockquoteIcon from '../../Icons/BlockquoteIcon'
import BoldIcon from '../../Icons/BoldIcon'
import BulletListIcon from '../../Icons/BulletListIcon'
import CheckIcon from '../../Icons/CheckIcon'
import ItalicIcon from '../../Icons/ItalicIcon'
import NumberedListIcon from '../../Icons/NumberedListIcon'
import JsonObjectIcon from '../../Icons/JsonObjectIcon'
import LinkIcon from '../../Icons/LinkIcon'
import TextFormatIcon from '../../Icons/TextFormatIcon'
import UnderlineIcon from '../../Icons/UnderlineIcon'
const BLOCK_TYPE_OPTIONS = [
{ label: 'Paragraph', value: 'paragraph' },
{ label: 'Heading 1', value: 'h1' },
{ label: 'Heading 2', value: 'h2' },
{ label: 'Heading 3', value: 'h3' },
{ label: 'Heading 4', value: 'h4' },
{ label: 'Quote', value: 'quote' }
]
const getBlockTypeValue = (editor) => {
if (!editor) {
return 'paragraph'
}
if (editor.isActive('blockquote')) {
return 'quote'
}
for (const level of [1, 2, 3, 4]) {
if (editor.isActive('heading', { level })) {
return `h${level}`
}
}
return 'paragraph'
}
const VIEW_ITEMS = [
{ key: 'editor', label: 'Editor' },
{ key: 'code', label: 'Code' }
]
const MarkdownToolbar = ({
editor,
size = 'small',
showCard = true,
viewState = { editor: true, code: false },
onViewStateChange = () => {},
editingToolsDisabled = false
}) => {
const [linkPopoverOpen, setLinkPopoverOpen] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const editorState = useEditorState({
editor,
selector: ({ editor }) => ({
blockTypeValue: getBlockTypeValue(editor),
isBold: editor?.isActive('bold') ?? false,
isItalic: editor?.isActive('italic') ?? false,
isUnderline: editor?.isActive('underline') ?? false,
isBulletList: editor?.isActive('bulletList') ?? false,
isOrderedList: editor?.isActive('orderedList') ?? false
})
})
const handleViewStateChange = (key, checked) => {
const nextState = { ...viewState, [key]: checked }
if (!nextState.editor && !nextState.code) {
nextState[key] = true
}
onViewStateChange(nextState)
}
const handleLinkPopoverOpenChange = (open) => {
setLinkPopoverOpen(open)
if (open) {
setLinkUrl(editor?.getAttributes('link').href ?? '')
}
}
const handleLinkSubmit = () => {
if (!editor) {
return
}
const trimmedUrl = linkUrl.trim()
if (!trimmedUrl) {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
} else {
editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: trimmedUrl })
.run()
}
setLinkPopoverOpen(false)
}
const handleBlockTypeChange = (nextBlockType) => {
if (!editor) {
return
}
const chain = editor.chain().focus()
if (editor.isActive('blockquote') && nextBlockType !== 'quote') {
chain.toggleBlockquote()
}
if (nextBlockType === 'paragraph') {
chain.setParagraph().run()
return
}
if (nextBlockType === 'quote') {
if (editor.isActive('heading')) {
chain.setParagraph()
}
chain.toggleBlockquote().run()
return
}
const headingLevel = Number(nextBlockType.replace('h', ''))
chain.setHeading({ level: headingLevel }).run()
}
const linkPopoverContent = (
<Space.Compact block style={{ width: 280 }}>
<Input
placeholder='Enter a URL...'
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onPressEnter={handleLinkSubmit}
style={{ flex: 1 }}
/>
<Button
type='primary'
onClick={handleLinkSubmit}
icon={<CheckIcon />}
style={{ minWidth: 32 }}
/>
</Space.Compact>
)
const cardContent = (
<Space wrap size={8}>
<Popover
content={
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{VIEW_ITEMS.map((item) => (
<Checkbox
checked={viewState[item.key]}
key={item.key}
onChange={(e) =>
handleViewStateChange(item.key, e.target.checked)
}
>
{item.label}
</Checkbox>
))}
</Flex>
}
placement='bottomLeft'
trigger='hover'
arrow={false}
>
<Button size={size} style={{ minWidth: 32 }}>
View
</Button>
</Popover>
<Select
size={size}
value={editorState.blockTypeValue}
options={BLOCK_TYPE_OPTIONS}
onChange={handleBlockTypeChange}
prefix={<TextFormatIcon />}
style={{ width: 160 }}
status={'defult'}
disabled={!editor || editingToolsDisabled}
/>
<Space.Compact size='small'>
<Tooltip content='Bold'>
<Button
size={size}
type={editorState.isBold ? 'primary' : 'default'}
icon={<BoldIcon />}
onClick={() => editor?.chain().focus().toggleBold().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip content='Italic'>
<Button
size={size}
type={editorState.isItalic ? 'primary' : 'default'}
icon={<ItalicIcon />}
onClick={() => editor?.chain().focus().toggleItalic().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip content='Underline'>
<Button
size={size}
type={editorState.isUnderline ? 'primary' : 'default'}
icon={<UnderlineIcon />}
onClick={() => editor?.chain().focus().toggleUnderline().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
</Space.Compact>
<Space.Compact size='small'>
<Tooltip content='Bulleted list'>
<Button
size={size}
type={editorState.isBulletList ? 'primary' : 'default'}
icon={<BulletListIcon />}
onClick={() => editor?.chain().focus().toggleBulletList().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip content='Numbered list'>
<Button
size={size}
type={editorState.isOrderedList ? 'primary' : 'default'}
icon={<NumberedListIcon />}
onClick={() => editor?.chain().focus().toggleOrderedList().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
</Space.Compact>
<Tooltip content='Block quote'>
<Button
size={size}
type={editorState.blockTypeValue === 'quote' ? 'primary' : 'default'}
icon={<BlockquoteIcon />}
onClick={() =>
handleBlockTypeChange(
editorState.blockTypeValue === 'quote' ? 'paragraph' : 'quote'
)
}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
<Popover
content={linkPopoverContent}
placement='bottom'
trigger='click'
arrow={false}
open={linkPopoverOpen}
styles={{ body: { borderRadius: '22px' } }}
onOpenChange={handleLinkPopoverOpenChange}
>
<Tooltip content='Insert link'>
<Button
size={size}
icon={<LinkIcon />}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
</Popover>
<Tooltip content='Insert code block'>
<Button
size={size}
icon={<JsonObjectIcon />}
onClick={() => editor?.chain().focus().toggleCodeBlock().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip content='Insert horizontal rule'>
<Button
size={size}
icon={<MinusOutlined />}
onClick={() => editor?.chain().focus().setHorizontalRule().run()}
disabled={!editor || editingToolsDisabled}
style={{ minWidth: 32 }}
/>
</Tooltip>
</Space>
)
return (
<>
{showCard ? (
<Card
style={{
width: '100%',
borderRadius: '12px 12px 0 0',
borderTop: 'none',
borderLeft: 'none',
borderRight: 'none'
}}
styles={{
body: {
padding: 12
}
}}
>
{cardContent}
</Card>
) : (
cardContent
)}
</>
)
}
MarkdownToolbar.propTypes = {
editor: PropTypes.shape({
chain: PropTypes.func,
getAttributes: PropTypes.func,
isActive: PropTypes.func
}),
size: PropTypes.string,
showCard: PropTypes.bool,
viewState: PropTypes.object,
onViewStateChange: PropTypes.func,
editingToolsDisabled: PropTypes.bool
}
export default MarkdownToolbar