---
title: "浮动元素"
description: "一个用于 Tiptap 编辑器的基础浮动 UI 组件，具备智能定位、选区跟踪和自动显示管理功能。"
canonical_url: "https://tiptap.zhcndoc.com/ui-components/utils-components/floating-element"
---

# 浮动元素

一个用于 Tiptap 编辑器的基础浮动 UI 组件，具备智能定位、选区跟踪和自动显示管理功能。

一个浮动的 UI 元素，会根据当前 Tiptap 编辑器中的选区位置自行定位。用于浮动工具栏、菜单及其它需出现在文本光标附近的 UI 元素，具备智能定位和交互处理功能。

> **Interactive demo:** [floating element](https://template.tiptap.dev/preview/tiptap-ui-utils/floating-element)

## 安装

通过 Tiptap CLI 添加该组件：

```bash
npx @tiptap/cli@latest add floating-element
```

## 组件

### `<FloatingElement />`

一个多功能 React 组件，可创建相对于 Tiptap 编辑器文本选区定位的浮动 UI 元素。

#### 使用示例

```tsx
import * as React from 'react'
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'

// --- Tiptap 核心扩展 ---
import { StarterKit } from '@tiptap/starter-kit'

// --- Tiptap UI ---
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { MarkButton } from '@/components/tiptap-ui/mark-button'

// --- UI 原语 ---
import { ButtonGroup } from '@/components/tiptap-ui-primitive/button'
import { Toolbar } from '@/components/tiptap-ui-primitive/toolbar'

// --- Tiptap 节点 ---
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss'

export const FloatingElementExample = () => {
  const editor = useEditor({
    immediatelyRender: false,
    content: `<h2>浮动元素示例</h2>
      <p>尝试选中编辑器中的一些文字。一个简单的格式工具栏将出现在你的选区上方。
      FloatingElement 组件会使 UI 元素根据文本选区或光标位置定位。
      它常用于上下文工具栏、菜单以及其他应在当前编辑上下文附近显示的元素。</p>`,
    extensions: [StarterKit],
  })

  return (
    <EditorContext.Provider value={{ editor }}>
      <EditorContent editor={editor} role="presentation" />

      <FloatingElement editor={editor}>
        <Toolbar variant="floating">
          <ButtonGroup orientation="horizontal">
            <MarkButton type="bold" />
            <MarkButton type="italic" />
          </ButtonGroup>
        </Toolbar>
      </FloatingElement>
    </EditorContext.Provider>
  )
}
```

#### 属性说明

| 名称                          | 类型                                    | 默认值                        | 说明                                                   |
| --------------------------- | ------------------------------------- | -------------------------- | ---------------------------------------------------- |
| `editor`                    | `Editor \| null`                      | `undefined`                | 需绑定的 Tiptap 编辑器实例                                    |
| `shouldShow`                | `boolean`                             | `undefined`                | 是否显示浮动元素的控制开关                                        |
| `floatingOptions`           | `Partial<UseFloatingOptions>`         | `undefined`                | 传递给浮动 UI 的额外配置项                                      |
| `zIndex`                    | `number`                              | `50`                       | 浮动元素的 z-index                                        |
| `onOpenChange`              | `(open: boolean) => void`             | `undefined`                | 显示状态变化时触发的回调                                         |
| `referenceElement`          | `HTMLElement \| null`                 | `undefined`                | 定位用的参考元素，若提供则优先于 getBoundingClientRect               |
| `getBoundingClientRect`     | `(editor: Editor) => DOMRect \| null` | `getSelectionBoundingRect` | 自定义获取浮动元素定位的函数，仅在未提供 referenceElement 时生效            |
| `closeOnEscape`             | `boolean`                             | `true`                     | 是否在按下 Esc 键时关闭浮动元素                                   |
| `resetTextSelectionOnClose` | `boolean`                             | `true`                     | 当为 `true`（默认）时，浮动元素关闭后会重置/清除编辑器的文本选区；为 `false` 则保留选区 |
| `children`                  | `React.ReactNode`                     | `undefined`                | 浮动元素内部显示的内容                                          |

## 高级使用示例

### 基础浮动工具栏

```tsx
import { shift, flip, offset } from '@floating-ui/react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'

function FloatingToolbar({ editor }) {
  return (
    <FloatingElement
      editor={editor}
      floatingOptions={{
        placement: 'top',
        middleware: [shift(), flip(), offset(8)],
      }}
    >
      {/* 浮动内容 */}
    </FloatingElement>
  )
}
```

### 支持移动端的自定义定位

```tsx
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { useMobile } from '@/hooks/use-mobile'

function ResponsiveFloatingMenu({ editor, isMenuVisible }) {
  const isMobile = useMobile()

  const getCustomRect = (editor) => {
    // 自定义定位逻辑
    // 示例：相对于当前光标定位
    return editor.view.coordsAtPos(editor.state.selection.from)
  }

  return (
    <FloatingElement
      editor={editor}
      shouldShow={isMenuVisible}
      getBoundingClientRect={getCustomRect}
      {...(isMobile
        ? {
            style: {
              position: 'fixed',
              left: 0,
              right: 0,
              bottom: 0,
              margin: '.5rem',
              zIndex: 50,
            },
          }
        : {})}
    >
      {/* 浮动内容 */}
    </FloatingElement>
  )
}
```

### 自定义 `shouldShow` 浮动菜单

```tsx
import { useState, useEffect } from 'react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { isSelectionValid } from '@/lib/tiptap-collab-utils'

function SelectionMenu({ editor }) {
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    if (!editor) return

    const updateVisibility = () => {
      const hasSelection = !editor.state.selection.empty
      const isValidSelection = isSelectionValid(editor)
      setIsVisible(hasSelection && isValidSelection)
    }

    editor.on('selectionUpdate', updateVisibility)
    return () => editor.off('selectionUpdate', updateVisibility)
  }, [editor])

  return (
    <FloatingElement editor={editor} shouldShow={isVisible}>
      {/* 你的浮动内容 */}
    </FloatingElement>
  )
}
```

### 使用参考元素定位

将浮动元素绑定到某个特定 DOM 元素，而不是文本选区：

```tsx
import { useState } from 'react'
import { offset, flip, shift } from '@floating-ui/react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'

function ButtonWithTooltip() {
  const [buttonRef, setButtonRef] = useState<HTMLElement | null>(null)
  const [showTooltip, setShowTooltip] = useState(false)

  return (
    <>
      <button
        ref={setButtonRef}
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        悬停我
      </button>

      <FloatingElement
        editor={editor}
        referenceElement={buttonRef}
        shouldShow={showTooltip}
        floatingOptions={{
          placement: 'top',
          middleware: [offset(8), flip(), shift()],
        }}
      >
        <div className="tooltip">有用的提示内容</div>
      </FloatingElement>
    </>
  )
}
```

## 工具函数

### `getSelectionBoundingRect(editor)`

获取编辑器中当前选区的边界矩形。

**参数:**

- `editor` - Tiptap 编辑器实例

**返回:** `DOMRect \| null` - 当前选区的边界矩形

```tsx
import { getSelectionBoundingRect } from '@/lib/tiptap-collab-utils'

const rect = getSelectionBoundingRect(editor)
console.log('选区边界:', rect)
```

### `isSelectionValid(editor, selection?, excludedNodeTypes?)`

检查当前选区是否适合显示浮动元素。对空选区、代码块、排除的节点类型及表格单元格返回 `false`。

**参数:**

- `editor` - Tiptap 编辑器实例
- `selection`（可选）- 待验证的选区，默认使用 `editor.state.selection`
- `excludedNodeTypes`（可选）- 需要排除的节点类型数组，默认 `['imageUpload', 'horizontalRule']`

**返回:** `boolean` - 选区是否适合显示浮动元素

```tsx
import { isSelectionValid } from '@/lib/tiptap-collab-utils'

const shouldShow = isSelectionValid(editor)

// 使用自定义排除节点类型
const isValid = isSelectionValid(editor, undefined, ['image', 'video'])
```

### `isTextSelectionValid(editor)`

检查当前文本选区是否适合编辑。对空选区、代码块和节点选区返回 `false`。

**参数:**

- `editor` - Tiptap 编辑器实例

**返回:** `boolean` - 文本选区是否有效

```tsx
import { isTextSelectionValid } from '@/lib/tiptap-collab-utils'

const canEdit = isTextSelectionValid(editor)
if (canEdit) {
  // 显示文本编辑工具栏
}
```

### `isElementWithinEditor(editor, element)`

检查某个 DOM 元素是否位于编辑器的 DOM 树内。用于判断点击或聚焦事件的目标。

**参数:**

- `editor` - Tiptap 编辑器实例
- `element` - 要检查的 DOM 元素

**返回:** `boolean` - 元素是否位于编辑器内

```tsx
import { isElementWithinEditor } from '@/components/tiptap-ui-utils/floating-element'

const handleClick = (event: MouseEvent) => {
  if (isElementWithinEditor(editor, event.target as Node)) {
    console.log('点击发生在编辑器内')
  }
}
```
