浮动元素
一个浮动的 UI 元素,会根据当前 Tiptap 编辑器中的选区位置自行定位。用于浮动工具栏、菜单及其它需出现在文本光标附近的 UI 元素,具备智能定位和交互处理功能。
安装
通过 Tiptap CLI 添加该组件:
npx @tiptap/cli@latest add floating-element组件
<FloatingElement />
一个多功能 React 组件,可创建相对于 Tiptap 编辑器文本选区定位的浮动 UI 元素。
使用示例
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 | 浮动元素内部显示的内容 |
高级使用示例
基础浮动工具栏
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>
)
}支持移动端的自定义定位
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 浮动菜单
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 元素,而不是文本选区:
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 - 当前选区的边界矩形
import { getSelectionBoundingRect } from '@/lib/tiptap-collab-utils'
const rect = getSelectionBoundingRect(editor)
console.log('选区边界:', rect)isSelectionValid(editor, selection?, excludedNodeTypes?)
检查当前选区是否适合显示浮动元素。对空选区、代码块、排除的节点类型及表格单元格返回 false。
参数:
editor- Tiptap 编辑器实例selection(可选)- 待验证的选区,默认使用editor.state.selectionexcludedNodeTypes(可选)- 需要排除的节点类型数组,默认['imageUpload', 'horizontalRule']
返回: boolean - 选区是否适合显示浮动元素
import { isSelectionValid } from '@/lib/tiptap-collab-utils'
const shouldShow = isSelectionValid(editor)
// 使用自定义排除节点类型
const isValid = isSelectionValid(editor, undefined, ['image', 'video'])isTextSelectionValid(editor)
检查当前文本选区是否适合编辑。对空选区、代码块和节点选区返回 false。
参数:
editor- Tiptap 编辑器实例
返回: boolean - 文本选区是否有效
import { isTextSelectionValid } from '@/lib/tiptap-collab-utils'
const canEdit = isTextSelectionValid(editor)
if (canEdit) {
// 显示文本编辑工具栏
}isElementWithinEditor(editor, element)
检查某个 DOM 元素是否位于编辑器的 DOM 树内。用于判断点击或聚焦事件的目标。
参数:
editor- Tiptap 编辑器实例element- 要检查的 DOM 元素
返回: boolean - 元素是否位于编辑器内
import { isElementWithinEditor } from '@/components/tiptap-ui-utils/floating-element'
const handleClick = (event: MouseEvent) => {
if (isElementWithinEditor(editor, event.target as Node)) {
console.log('点击发生在编辑器内')
}
}