构建自定义查找和替换界面
Editor
查找和替换扩展是无头的。它可以搜索、高亮、导航和替换文本,但不会渲染界面。你可以自行决定控件的位置:工具栏、模态框、侧边栏、浮动面板,或由键盘驱动的工作流。
本指南将构建一个小型界面,其中包含搜索和替换输入框、匹配选项、结果导航以及替换操作。
UI 如何连接到扩展
该扩展提供了用于更新搜索内容和执行操作的命令。其当前状态可通过 editor.storage.findAndReplace 获取。
使用命令处理输入和按钮事件:
editor.commands.setSearchTerm('Tiptap')
editor.commands.goToNextResult()
editor.commands.replaceAll()读取存储以显示选中的结果,并在没有匹配项时禁用操作:
const { currentIndex, results } = editor.storage.findAndReplace
const resultLabel =
results.length === 0 ? 'No results' : `${(currentIndex ?? 0) + 1} of ${results.length}`React
使用 useEditorState 仅订阅控件所需的查找和替换存储值。这样可以让 UI 保持同步,同时避免因无关的编辑器更改而重新渲染。
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
import FindAndReplace from '@tiptap/extension-find-and-replace'
import StarterKit from '@tiptap/starter-kit'
function FindAndReplaceControls({ editor }) {
const state = useEditorState({
editor,
selector: (context) => ({
searchTerm: context.editor.storage.findAndReplace.searchTerm,
replaceTerm: context.editor.storage.findAndReplace.replaceTerm,
caseSensitive: context.editor.storage.findAndReplace.caseSensitive,
useRegex: context.editor.storage.findAndReplace.useRegex,
wholeWord: context.editor.storage.findAndReplace.wholeWord,
resultCount: context.editor.storage.findAndReplace.results.length,
currentIndex: context.editor.storage.findAndReplace.currentIndex,
}),
})
const resultLabel =
state.resultCount === 0
? '无结果'
: `${(state.currentIndex ?? 0) + 1} / ${state.resultCount}`
return (
<div className="find-and-replace">
<input
aria-label="搜索"
placeholder="搜索"
value={state.searchTerm}
onChange={(event) => editor.commands.setSearchTerm(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key !== 'Enter') return
if (event.shiftKey) {
editor.commands.goToPreviousResult()
} else {
editor.commands.goToNextResult()
}
}}
/>
<input
aria-label="替换"
placeholder="替换"
value={state.replaceTerm}
onChange={(event) => editor.commands.setReplaceTerm(event.currentTarget.value)}
/>
<label>
<input
type="checkbox"
checked={state.caseSensitive}
onChange={(event) => editor.commands.setCaseSensitive(event.currentTarget.checked)}
/>
匹配大小写
</label>
<label>
<input
type="checkbox"
checked={state.wholeWord}
disabled={state.useRegex}
onChange={(event) => editor.commands.setWholeWord(event.currentTarget.checked)}
/>
全字匹配
</label>
<label>
<input
type="checkbox"
checked={state.useRegex}
onChange={(event) => editor.commands.setUseRegex(event.currentTarget.checked)}
/>
正则表达式
</label>
<button
disabled={state.resultCount === 0}
onClick={() => editor.commands.goToPreviousResult()}
>
上一个
</button>
<button disabled={state.resultCount === 0} onClick={() => editor.commands.goToNextResult()}>
下一个
</button>
<button disabled={state.resultCount === 0} onClick={() => editor.commands.replace()}>
替换
</button>
<button disabled={state.resultCount === 0} onClick={() => editor.commands.replaceAll()}>
全部替换
</button>
<button onClick={() => editor.commands.clearSearch()}>清除</button>
<span>{resultLabel}</span>
</div>
)
}
export default function FindAndReplaceEditor() {
const editor = useEditor({
extensions: [StarterKit, FindAndReplace],
content: '<p>在本文档中搜索 Tiptap。</p>',
})
if (!editor) {
return null
}
return (
<>
<FindAndReplaceControls editor={editor} />
<EditorContent editor={editor} />
</>
)
}Vue
同一个接口可以在模板中读取编辑器存储,并在事件处理器中调用命令。此示例使用 Vue 的 Options API,但命令和存储在 Composition API 中的工作方式相同。
<template>
<div v-if="editor">
<div class="find-and-replace">
<input
aria-label="搜索"
placeholder="搜索"
:value="editor.storage.findAndReplace.searchTerm"
@input="(event) => editor.commands.setSearchTerm(event.target.value)"
@keydown.enter.exact="editor.commands.goToNextResult()"
@keydown.shift.enter="editor.commands.goToPreviousResult()"
/>
<input
aria-label="替换"
placeholder="替换"
:value="editor.storage.findAndReplace.replaceTerm"
@input="(event) => editor.commands.setReplaceTerm(event.target.value)"
/>
<label>
<input
type="checkbox"
:checked="editor.storage.findAndReplace.caseSensitive"
@change="(event) => editor.commands.setCaseSensitive(event.target.checked)"
/>
区分大小写
</label>
<label>
<input
type="checkbox"
:checked="editor.storage.findAndReplace.wholeWord"
:disabled="editor.storage.findAndReplace.useRegex"
@change="(event) => editor.commands.setWholeWord(event.target.checked)"
/>
全字匹配
</label>
<label>
<input
type="checkbox"
:checked="editor.storage.findAndReplace.useRegex"
@change="(event) => editor.commands.setUseRegex(event.target.checked)"
/>
正则表达式
</label>
<button :disabled="hasNoResults" @click="editor.commands.goToPreviousResult()">
上一个
</button>
<button :disabled="hasNoResults" @click="editor.commands.goToNextResult()">下一个</button>
<button :disabled="hasNoResults" @click="editor.commands.replace()">替换</button>
<button :disabled="hasNoResults" @click="editor.commands.replaceAll()">全部替换</button>
<button @click="editor.commands.clearSearch()">清除</button>
<span>{{ resultLabel }}</span>
</div>
<editor-content :editor="editor" />
</div>
</template>
<script>
import FindAndReplace from '@tiptap/extension-find-and-replace'
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
export default {
components: { EditorContent },
data() {
return { editor: null }
},
computed: {
hasNoResults() {
return this.editor.storage.findAndReplace.results.length === 0
},
resultLabel() {
const { currentIndex, results } = this.editor.storage.findAndReplace
if (results.length === 0) {
return 'No results'
}
return `${(currentIndex ?? 0) + 1} of ${results.length}`
},
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit, FindAndReplace],
content: '<p>Search this document for Tiptap.</p>',
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>适配界面
这些示例中的控件只是其中一种选择。你可以将相同的命令和存储功能呈现在通过 Cmd/Ctrl+F 打开的对话框、持久侧边栏或紧凑工具栏中。
构建自己的 UI 时,启用正则表达式模式后,请禁用或隐藏全词匹配功能。正则表达式会改变匹配项的查找方式,但替换文本始终会按字面插入。