likes
comments
collection
share

基于tiptap实现富文本编辑器 - 文字浮动菜单(2)文字颜色选择器 我们之前已经安装了Color插件,已经支持了文字

作者站长头像
站长
· 阅读数 38

项目地址: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>

基于tiptap实现富文本编辑器 - 文字浮动菜单(2)文字颜色选择器 我们之前已经安装了Color插件,已经支持了文字

超链接编辑组件

当文字还不是超链接的时候,需要在文字浮动菜单上有将普通文本转为超链接的组件。

新建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就已经把超链接元素过滤了,所以不会展示。

基于tiptap实现富文本编辑器 - 文字浮动菜单(2)文字颜色选择器 我们之前已经安装了Color插件,已经支持了文字

基于tiptap实现富文本编辑器 - 文字浮动菜单(2)文字颜色选择器 我们之前已经安装了Color插件,已经支持了文字

转载自:https://juejin.cn/post/7419762259209994278
评论
请登录