- 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.
348 lines
9.3 KiB
JavaScript
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
|