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;
}
相关推荐
小四的小六2 小时前
WebView 兼容性踩坑实录:那些让我加班的坑
javascript·webview
jump_jump2 小时前
用官方模板理解 Decky 插件:一次从模板到架构的速览
javascript·python·游戏
张元清2 小时前
React 表单处理:防抖校验、自动保存草稿与受控输入
前端·javascript·面试
Lee川2 小时前
React 首页秒开优化:用 KeepAlive 实现丝滑的页面缓存
前端·react.js
Hilaku2 小时前
给技术团队定规范,为什么 90% 最后都变成了走形式?
前端·javascript·程序员
小番茄夫斯基2 小时前
Node.js 从零开发 MCP 服务:30 分钟上手,对接 Claude/Cursor 全流程
前端·mcp
LIO2 小时前
一套代码,多端并行——uni-app + Vue3 多端开发完全指南
前端·vue.js·uni-app
众创岛2 小时前
web自动化中的日志模块
java·前端·自动化
昼猫2 小时前
前端打印分页技术探讨与 PrintomJs 方案
javascript·浏览器