Tiptap之造字组件

Tiptap 自定义扩展

Tiptap 的强大之处在于其扩展机制,开发者可以通过以下方式自定义功能:

  1. 继承现有扩展:通过 extend 方法扩展现有节点或标记
  2. 创建新扩展:定义全新的节点或标记类型
  3. 添加属性:为现有扩展添加自定义属性
  4. 重写方法:覆盖默认的行为实现

造字组件

使用继承现有扩展方式创建造字组件,主要用于在文本流中插入和展示那些无法通过常规输入法输入的特殊字符、图标或自定义图形。它实际上是一个特殊的图片节点,用于在文本中插入一个代表特定字符的图片,并且有替换文本(alt)属性

造字组件扩展

造字组件扩展可以直接继承官方 Image 组件,然后添加自定义属性。

需要多一个 glyph 字段就行,能展示替换文本,其实可以直接使用 alt 属性也行。

目前是有两种方案:

  1. 自定义扩展:直接把 extension-image 拷贝过来,在其基础上更改
  2. 继承官方扩展:继承官方 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;
}
相关推荐
东方小月4 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
Csvn4 小时前
🧩 ESM vs CJS 混用的 7 个「天坑」——从 TypeScript 编译到 Node 与浏览器
前端
Csvn4 小时前
🎯 Web 性能 API 集合:Performance Observer 的 5 个冷门妙用
前端
Csvn4 小时前
深入 React 闭包陷阱:从根源上理解并根治 stale closure
前端
whyfail4 小时前
前端学 Spring Boot(8):接口为什么越用越慢?
前端·spring boot·后端
用户059540174465 小时前
LangChain 记忆测试踩坑实录:这两个坑让我排查了 4 小时
前端·css
程序员黑豆5 小时前
鸿蒙应用开发:@Monitor 装饰器使用教程
前端·harmonyos
用户938515635075 小时前
从零构建《天龙八部》知识库:EPUB 加载→文本分割→向量嵌入→Milvus 存储→RAG 问答,一条链路打通
javascript·人工智能·全栈
SamChan906 小时前
在Web应用中集成PDF多语言翻译功能:PDFTranslator API实战指南
前端·python·ai·pdf·yapi·机器翻译
kyriewen6 小时前
AI Agent 9秒删光了生产数据库——我给自己的项目做了5个紧急检查
前端·ai编程·claude