基于tiptap实现富文本编辑器 - 文字浮动菜单(2)文字颜色选择器 我们之前已经安装了Color插件,已经支持了文字
项目地址:tiptap-editor
效果展示:tiptap-editor-tuntun.netlify.app/
持续更新中
文字颜色选择器
我们之前已经安装了Color插件,已经支持了文字颜色的支持。所以我们可以直接编写组件,首先写一个颜色选择器组件,这里我们使用react-colorful
这个库为基础来封装颜色选择器。
import { Icon } from './icon'
import { memo } from 'react'
import { HexColorInput, HexColorPicker } from 'react-colorful'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
export const ColorPicker = ({
color,
onChange,
onReset,
}: {
color: string
onChange?: (color: string) => void
onReset?: () => void
}) => {
return (
<div className="w-full flex flex-col gap-2">
<HexColorPicker color={color} onChange={onChange} />
<div className="w-full flex items-center gap-2">
<div
className={cn(
'w-[calc(200px_-_2.5rem)] overflow-hidden flex-1 h-8 outline-none border border-input rounded-sm px-2 py-0.5 flex items-center gap-2 placeholder:text-muted-foreground'
)}>
<span className="text-muted-foreground">#</span>
<HexColorInput
className="outline-none border-none"
placeholder="请输入颜色"
color={color}
onChange={onChange}
/>
</div>
<Tooltip>
<TooltipTrigger>
<Button
variant="outline"
size="icon"
className="w-8 h-8"
onClick={onReset}>
<Icon name="Undo" />
</Button>
</TooltipTrigger>
<TooltipContent>
<span>重置</span>
</TooltipContent>
</Tooltip>
</div>
</div>
)
}
export const MemoColorPicker = memo(ColorPicker)
之后在Textmenu中添加ColorPicker和Popover
const states: {
currentColor: string
} = useEditorState({
editor,
selector: (ctx) => {
return {
currentColor: ctx.editor.getAttributes('textStyle')?.color || '#000000',
}
},
})
const onResetColor = useCallback(() => {
editor.chain().unsetColor().run()
}, [editor])
const onChangeColor = useCallback(
(color: string) => {
editor.chain().setColor(color).run()
},
[editor]
)
<Popover>
<PopoverTrigger asChild>
<div>
<ToggleButton
pressed={states.currentColor !== '#000000'}
tooltip="文字颜色">
<Icon name="Palette" />
</ToggleButton>
</div>
</PopoverTrigger>
<PopoverContent
asChild
align="center"
sideOffset={8}
className="p-1 w-[200px_+_0.5rem] bg-white dark:bg-zinc-800">
<div>
<MemoColorPicker
color={states.currentColor}
onChange={onChangeColor}
onReset={onResetColor}
/>
</div>
</PopoverContent>
</Popover>
文字背景
文字背景的支持需要使用@tiptap/extension-highlight
插件
pnpm install @tiptap/extension-highlight
之后添加插件,这里配置了highlight的默认class和支持多颜色。
Highlight.configure({
HTMLAttributes: {
class: 'rounded-sm',
},
multicolor: true,
}),
之后的流程和文字颜色选择器一样,在TextMenu中添加文字背景选择器和Popover
const states: {
currentColor: string
currentHighlight: string
} = useEditorState({
editor,
selector: (ctx) => {
return {
currentColor: ctx.editor.getAttributes('textStyle')?.color || '#000000',
currentHighlight:
ctx.editor.getAttributes('highlight')?.color || undefined,
}
},
})
const onChangeHighlight = useCallback(
(color: string) => {
editor.chain().setHighlight({ color }).run()
},
[editor]
)
const onResetHighlight = useCallback(() => {
editor.chain().unsetHighlight().run()
}, [editor])
<Popover>
<PopoverTrigger asChild>
<div>
<ToggleButton
pressed={states.currentHighlight !== undefined}
tooltip="文字背景">
<Icon name="Highlighter" />
</ToggleButton>
</div>
</PopoverTrigger>
<PopoverContent
asChild
align="center"
sideOffset={8}
className="p-1 w-[200px_+_0.5rem] bg-white dark:bg-zinc-800">
<div>
<MemoColorPicker
color={states.currentHighlight}
onChange={onChangeHighlight}
onReset={onResetHighlight}
/>
</div>
</PopoverContent>
</Popover>
超链接编辑组件
当文字还不是超链接的时候,需要在文字浮动菜单上有将普通文本转为超链接的组件。
新建src/components/editor/menu/link-editor.tsx
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Icon } from './icon'
import { Input } from '@/components/ui/input'
import { memo, useState } from 'react'
import { ToggleButton } from './toggle-button'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
export const LinkEditor = ({
onConfirm,
}: {
onConfirm?: (url: string, openInNewTab: boolean) => void
}) => {
const [url, setUrl] = useState('')
const [openInNewTab, setOpenInNewTab] = useState(false)
const handleConfirm = () => {
onConfirm?.(url, openInNewTab)
setUrl('')
setOpenInNewTab(false)
}
return (
<Popover>
<PopoverTrigger>
<ToggleButton pressed={false}>
<Icon name="Link" />
</ToggleButton>
</PopoverTrigger>
<PopoverContent sideOffset={8}>
<div className="flex flex-col gap-2">
<Input
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<div className="flex items-center space-x-2">
<Switch
id="open-in-new-tab"
checked={openInNewTab}
onCheckedChange={(checked) => setOpenInNewTab(checked)}
/>
<Label htmlFor="open-in-new-tab">在新的标签页中打开</Label>
</div>
<Button
className="mt-2"
variant="outline"
size="sm"
onClick={handleConfirm}>
确定
</Button>
</div>
</PopoverContent>
</Popover>
)
}
export const MemoLinkEditor = memo(LinkEditor)
之后在Textmenu中导入:
const onEditLink = useCallback(
(url: string, openInNewTab: boolean) => {
editor
.chain()
.focus()
.setLink({ href: url, target: openInNewTab ? '_blank' : '' })
.run()
},
[editor]
)
<MemoLinkEditor onConfirm={onEditLink} />
就可以将普通文本转为超链接了
但编辑已经成为超链接的元素就需要额外的超链接浮动菜单来展示:
新建src/components/editor/menu/link-menu.tsx
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { BubbleMenu, Editor, useEditorState } from '@tiptap/react'
import { useCallback, useState } from 'react'
import { Icon } from './icon'
export const LinkMenu = ({ editor }: { editor: Editor }) => {
const { link, target } = useEditorState({
editor,
selector: (ctx) => {
const attrs = ctx.editor.getAttributes('link')
return { link: attrs.href, target: attrs.target }
},
})
const [isEditing, setIsEditing] = useState(false)
const [url, setUrl] = useState<string | null>(null)
const [openInNewTab, setOpenInNewTab] = useState<boolean | null>(null)
const shouldShow = useCallback(() => {
const isActive = editor.isActive('link')
return isActive
}, [editor])
const handleConfirm = () => {
if (url) {
editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: url, target: openInNewTab ? '_blank' : '' })
.run()
setIsEditing(false)
}
}
const handleUnsetLink = () => {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
setIsEditing(false)
}
return (
<BubbleMenu
editor={editor}
shouldShow={shouldShow}
tippyOptions={{
placement: 'bottom',
offset: [0, 0],
maxWidth: 'calc(100vw - 16px)',
appendTo: document.body,
onHidden: () => {
setIsEditing(false)
setUrl(null)
setOpenInNewTab(null)
},
}}>
<div className="flex flex-row items-center gap-1 p-2 border bg-white rounded-md dark:bg-zinc-800">
{isEditing ? (
<div className="flex flex-col gap-2 w-[300px]">
<Input
placeholder="https://example.com"
value={url ?? ''}
onChange={(e) => setUrl(e.target.value)}
className="focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<div className="flex items-center space-x-2">
<Switch
id="open-in-new-tab"
checked={openInNewTab ?? false}
onCheckedChange={(checked) => setOpenInNewTab(checked)}
/>
<Label htmlFor="open-in-new-tab">在新的标签页中打开</Label>
</div>
<Button
className="mt-2"
variant="outline"
size="sm"
onClick={handleConfirm}>
确定
</Button>
</div>
) : (
<div className="flex flex-col gap-2">
<a
href={link}
target={target}
title={link}
className="text-sm text-blue-500 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]">
{link}
</a>
<div className="flex flex-row items-center gap-1">
<Button
className="flex items-center gap-1"
variant="outline"
size="sm"
onClick={handleUnsetLink}>
<Icon name="Unlink" />
取消链接
</Button>
<Button
className="flex items-center gap-1"
variant="outline"
size="sm"
onClick={() => {
setUrl(link)
setOpenInNewTab(target === '_blank')
setIsEditing(true)
}}>
<Icon name="Pencil" />
编辑
</Button>
</div>
</div>
)}
</div>
</BubbleMenu>
)
}
这样在点击超链接的时候就会展示超链接菜单,原本的文字菜单我们在上一章中配置shouldShow
就已经把超链接元素过滤了,所以不会展示。
转载自:https://juejin.cn/post/7419762259209994278