Tiptap 自定义扩展
Tiptap 的强大之处在于其扩展机制,开发者可以通过以下方式自定义功能:
- 继承现有扩展:通过 extend 方法扩展现有节点或标记
- 创建新扩展:定义全新的节点或标记类型
- 添加属性:为现有扩展添加自定义属性
- 重写方法:覆盖默认的行为实现
造字组件
使用继承现有扩展方式创建造字组件,主要用于在文本流中插入和展示那些无法通过常规输入法输入的特殊字符、图标或自定义图形。它实际上是一个特殊的图片节点,用于在文本中插入一个代表特定字符的图片,并且有替换文本(alt)属性
造字组件扩展
造字组件扩展可以直接继承官方 Image 组件,然后添加自定义属性。
需要多一个 glyph 字段就行,能展示替换文本,其实可以直接使用 alt 属性也行。
目前是有两种方案:
- 自定义扩展:直接把 extension-image 拷贝过来,在其基础上更改
- 继承官方扩展:继承官方 Image 节点,然后添加自定义属性
我选择了第二种,并且直接复用 alt 属性,减少改动,保证稳定性和兼容性。
jsx
import { Image as TiptapImage } from "@tiptap/extension-image";
import "./index.scss";
export const GlyphImage = TiptapImage.extend({
name: "glyphImage",
addOptions() {
return {
...super.addOptions?.(),
inline: true, // 强制设置为行内,确保可以在文字中间显示
HTMLAttributes: { class: "glyph-image" },
};
},
addCommands() {
return {
...super.addCommands?.(),
// 新增方法
setGlyphImage:
(options) =>
({ commands }) => {
return commands.insertContent({ type: this.name, attrs: options });
},
};
},
});
css
.glyph-image {
display: inline !important; /* 强制行内显示 */
/* width: 1em; */
border: 1px solid #bae6fd; /* 可视化边界 */
}
造字组件使用
jsx
import { GlyphImage } from "@/components/tiptap-ui/glyph-image/extension-glyph-image";
// 在编辑器配置中注册组件
const editor = useEditor({ extensions: [GlyphImage] });
// 使用命令插入造字组件
editor.commands.setGlyphImage({
src: "https://placehold.co/40x40/6A00F5/white",
alt: "造字替换文本", // 替换文本
title: "造字标题", // 标题
});
json 数据展示:
json
{
"type": "glyphImage",
"attrs": {
"src": "/pdf/1-1-2.png",
"alt": "造字替换文本",
"title": "造字标题"
}
},

造字组件弹框
同"脚注组件"一样,参照"链接组件"改造:
- 图片地址 src:可以直接输入地址,也可以上传图片
- 替换文本 alt:复用 alt 属性作为替换文本,利用 title 属性提供鼠标悬停提示
jsx
// glyph-image-popover.tsx文件
const GlyphImageMain: React.FC<GlyphImageMainProps> = ({
src,
setSrc,
alt,
setAlt,
setGlyph,
glyphUpload,
isActive,
uploading,
uploadProgress,
}) => {
const fileInputRef = React.useRef < HTMLInputElement > null;
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
try {
await glyphUpload(file);
} finally {
// 清空input,允许重复选择同一文件
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
}
};
const handleUploadClick = () => {
if (!uploading) {
fileInputRef.current?.click();
}
};
return (
<Card>
<CardBody>
<CardItemGroup>
<Input
type="url"
placeholder="输入图片地址(src)"
value={src}
onChange={(e) => setSrc(e.target.value)}
/>
<Input
type="text"
placeholder="输入替换文本(alt)"
value={alt}
onChange={(e) => setAlt(e.target.value)}
/>
<ButtonGroup orientation="horizontal" className="justify-end mt-2">
<Button
type="button"
onClick={handleUploadClick}
title="上传图片"
data-style="outline"
disabled={uploading}
>
{uploading ? `上传中${Math.round(uploadProgress)}%` : "上传图片"}
</Button>
{/* 隐藏的文件输入 */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: "none" }}
/>
<Button
type="button"
onClick={setGlyph}
title="保存造字"
disabled={!src && !isActive}
data-style="outline"
className="ml-2"
>
保存
</Button>
</ButtonGroup>
</CardItemGroup>
</CardBody>
</Card>
);
};

图片上传时,不使用 base64 保存图片,而是通过 OSS 保存到阿里云服务器,富文本组件中置保存地址即可
use-glyph-image-popover.ts文件


/lib/tiptap-utils.ts文件

最终效果,如下图所示:

造字组件高亮问题
选中图片的时候,造字组件是高亮的,需要修复。

主要是修改canSetGlyph方法:脚注组件也是类似的,修改canSetFootnote即可
jsx
// 检查是否可以设置造字
export function canSetGlyph(editor: Editor | null): boolean {
// 基础校验:编辑器是否存在或者编辑器是否可编辑
if (!editor || !editor.isEditable) return false;
// 节点合法性检测
// - 检查"glyphImage"节点是否在编辑器的schema中注册(确保功能支持)
// - 检查当前选中的节点是否为"image"类型(避免与普通图片冲突)
if (
!isNodeInSchema("glyphImage", editor) ||
isNodeTypeSelected(editor, ["image"])
)
return false;
// 最终校验:调用编辑器的can方法检查是否可以执行setGlyphImage命令
return editor.can().setGlyphImage?.() || false;
}