实现在富文本中直接Ctrl+C复制图片并自动上传,并支持HTML格式的图片的复制

需求背景:

希望实现在富文本中直接Ctrl+C复制图片并自动上传,如果从石墨文档或者地方粘贴过来的HTML格式的图片希望也能和本地上传的图片样式保持统一,即缩略图展示,支持放大操作。

实现

本地图片的复制,上传,loadIng占位图展示及最终上传图片的展示相关逻辑的实现

TipTap有个FileHandler extension,专门处理文件的拖拽,复制逻辑。

javascript 复制代码
const editor = useEditor({
      extensions: [
        // 其他逻辑
        // FileHandler
        FileHandler.configure({
          allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
          onPaste: (currentEditor, files, htmlContent) => {
            files.forEach((file) => {
              if (htmlContent) {
                return false;
              }
              const fileReader = new FileReader();
              fileReader.readAsDataURL(file);
              fileReader.onload = () => {
                // eslint-disable-next-line @typescript-eslint/no-use-before-define
                // 处理上传过程中占位图的展示和图片的上传
                pasteFile(file);
              };
            });
          },
        }),
       ],
    });

上传图片的过程中展示一个loadIng的图片,在最开始的版本中尝试通过计算当前光标所在的位置以及loading 图片的占位大小来实现上传过程中展示占位图,上传完毕删除占位图的效果。但是发现在富文本不同位置如文字中间,文字末尾,段落末尾,新增行开始处占位图的位置和占位大小各不相同,实现半天都不对,转而使用AI提供的另外一种思路去实现,详细看下方逻辑。

typescript 复制代码
const pasteFile = async (file: File) => {
      // 生成一个随机的loadIng图ID,方便后续查找
      const loadingId = generateLoadingId();
      const loadingSVG = createLoadingSVG();
      try {
        // 插入加载占位图
        editor
          ?.chain()
          .focus()
          .insertContent({
            type: 'image',
            attrs: {
              src: loadingSVG,
              alt: 'Loading...',
              'data-loading': loadingId,
            },
          })
          .run();
        
        // 调用现有上传图片方法
        const imageUrl = await uploadImage(file);
        // 先预加载下上传的图片
        await preLoadImagePromise(imageUrl || '');
        if (imageUrl) {
          // 替换为实际图片
          const imagePos = replaceLoadingImage(loadingId, imageUrl);
          // 在插入图片的后面设置光标
          if (imagePos) {
            editor.commands.setTextSelection(imagePos);
            editor.commands.focus();
          }
        } else {
          // 上传失败,删除占位图
          removeLoadingImage(loadingId);
        }
      } catch (error) {
        // ...
      }
    };
​
    // 替换loading占位图的实现
    const replaceLoadingImage = (loadingId: string, imageUrl: string) => {
      if (!editor) return;
      // TipTap是基于ProseMirror的高级封装,下面的这段由AI给出,有空还是得看下ProseMirror的文档。
      const { tr } = editor.state;
      let found = false;
      let imagePos: number | undefined;
      // 编辑富文本的所有节点,返回false终止遍历
      editor.state.doc.descendants((node, pos) => {
        if (found) return false;
        // 找到了占位的loadIng图片
        if (node.type.name === 'image' && node.attrs['data-loading'] === loadingId) {
          // 不计入操作栈,防止用户回撤操作出现占位图
          tr.setMeta('addToHistory', false);
          // 替换占位图节点的url,
          tr.setNodeMarkup(pos, undefined, {
            ...node.attrs,
            src: imageUrl,
            'data-loading': null, // 移除加载标识
            displayHeight: '100px', // 自定义高度
            displayWidth: 'auto',
            maxWidth: '100%',
          });
          // 记录图片的终止位置
          imagePos = pos + node.nodeSize;
          found = true;
          return false;
        }
        return true;
      });
      if (found) {
        editor.view.dispatch(tr);
        return imagePos;
      }
    };

正常展示缩略图,点击放大展示(直接套用Antd的Image)

AntD图片展示是使用图片的大图预览功能,不想再额外实现,这部分功能可以通过拓展TipTap 的Image extension实现。当我们想要在TipTap中渲染react组件,需要使用ReactNodeViewRenderer这个函数将组件包裹

AI给出的原因是:

架构差异桥接

Tiptap 基于 ProseMirror,它使用的是虚拟 DOM 和自己的节点系统,而 React 有自己的组件系统和渲染机制。ReactNodeViewRenderer 充当了两者之间的桥梁,解决了以下问题:

  • 渲染系统不兼容:ProseMirror 使用原生 DOM 操作,React 使用虚拟 DOM
  • 生命周期管理:需要将 ProseMirror 的节点生命周期与 React 组件生命周期同步
  • 状态管理:需要在两个不同的状态管理系统之间传递数据

文档链接\] [tiptap.dev/docs/editor...](https://link.juejin.cn?target=https%3A%2F%2Ftiptap.dev%2Fdocs%2Feditor%2Fextensions%2Fcustom-extensions%2Fnode-views%2Freact "https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react") ```typescript import { NodeViewWrapper } from '@tiptap/react'; import { Image as AntdImage } from 'antd'; ​ // 创建图片组件 export const ImageComponent = ({ node }: { node: any }) => { const handleImageClick = (e: React.MouseEvent) => { e.stopPropagation(); // 阻止事件冒泡 }; ​ // 从节点属性中获取自定义的显示尺寸,如果没有则使用默认值。这次需求全部展示固定高度,宽度根据图片比例自适应,最大不超过100%。 const displayWidth = node.attrs.displayWidth || 'auto'; const displayHeight = node.attrs.displayHeight || 'auto'; const maxWidth = node.attrs.maxWidth || '100%'; const maxHeight = node.attrs.maxHeight || 'none'; ​ return ( ); }; const editor = useEditor({ extensions: [ // 其他逻辑 Image.configure({ // 设置图片行内展示 inline: true, }).extend({ // 自定义渲染图片样式, addNodeView() { return ReactNodeViewRenderer(ImageComponent); }, addAttributes() { return { ...this.parent?.(), 'data-loading': { default: null, parseHTML: (element) => element.getAttribute('data-loading'), renderHTML: (attributes) => { if (!attributes['data-loading']) { return {}; } return { 'data-loading': attributes['data-loading'], }; }, }, displayWidth: { // ... }, displayHeight: { // ... }, maxWidth: { // ... }, maxHeight: { // ... }, }; }, }), ], // ... }); ``` ### 处理粘贴HTML格式的情况,预期效果(支持多种图片粘贴,图文混合粘贴,单张图粘贴) TipTap专门提供了函数处理HTML的粘贴----transformPastedHTML,这里只需要返回普通的img标签,和赋上正确的宽高属性,Image extension会帮我们自动处理剩下的逻辑。 ```css const editor = useEditor({ editorProps: { // 使用 transformPastedHTML 来处理粘贴的HTML内容 transformPastedHTML: (html: string) => { // 检测是否包含img标签 if (/]*src=["']([^"']*)["'][^>]*/?>/gi.test(html)) { // 将img标签转换为自定义属性格式,保留其他HTML结构 return html.replace(/]*?)src=["']([^"']*)["']([^>]*?)/?>/gi, (match, beforeSrc, src, afterSrc) => { const altMatch = match.match(/alt=["']([^"']*)["']/); const alt = altMatch ? altMatch[1] : ''; return `${alt}`; }); } return html; }, }, // ... }); ``` 这个需求的内容到此就结束了,看文档发现TipTap具有很强的扩展性,功能也很强大,还支持多人协作编辑,AI集成等等强大的功能,有空可以深入研究下。

相关推荐
wifi歪f2 小时前
🎨 探究Function Calling 和 MCP 的奥秘
前端·ai编程·mcp
BrendanDash2 小时前
React 19.2 已发布,现已上线 npm!
前端·react.js
sheji34162 小时前
【开题答辩全过程】以 Web数据挖掘在电子商务中的应用研究为例,包含答辩的问题和答案
前端·人工智能·数据挖掘
whltaoin2 小时前
Vue 与 React 深度对比:技术差异、选型建议与未来趋势
前端·前端框架·vue·react·技术选型
敲敲敲-敲代码3 小时前
web系统(asp.net和C#)
前端·c#·asp.net
IT_陈寒3 小时前
Python开发者必坑指南:3个看似聪明实则致命的‘优化’让我损失了50%性能
前端·人工智能·后端
初圣魔门首席弟子3 小时前
c++中this指针使用bug
前端·c++·bug
AI视觉网奇9 小时前
rknn yolo11 推理
前端·人工智能·python
gplitems1239 小时前
Gunslinger – Gun Store & Hunting WordPress Theme: A Responsible
开发语言·前端·javascript