You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Quill富文本编辑器实现社交媒体链接嵌入功能求助

实现React Quill将社交媒体链接转为嵌入内容

需求

希望React Quill富文本编辑器能将社交媒体链接(如TikTok链接https://www.tiktok.com/@epicgardening/video/7055411162212633903)转换为嵌入内容,最终输出为HTML字符串,效果类似react-social-media-embed库的展示形式。

现有代码

富文本编辑器组件代码

import { useCallback, useMemo, useEffect } from 'react';
import ImageResize from 'quill-image-resize-module-react';
import ReactQuill, { Quill } from 'react-quill';
import { message } from 'antd';

import { uploadFiles } from 'utils';
import 'react-quill/dist/quill.bubble.css';
import 'react-quill/dist/quill.snow.css';

import './styles.scss';

Quill.register('modules/imageResize', ImageResize);

const RichTextEditor = ({
  editorState,
  onChange,
  readOnly = false,
  setLoading = () => {}
}) => {
  window.Quill = Quill;
  let quillRef = null; // Quill instance
  let reactQuillRef = null; // ReactQuill component

  useEffect(() => {
    attachQuillRefs();
  }, []);

  const attachQuillRefs = useCallback(() => {
    if (typeof reactQuillRef.getEditor !== 'function') return;
    quillRef = reactQuillRef.getEditor();
  }, []);

  const imageHandler = () => {
    const input = document.createElement('input');

    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();

    input.onchange = async () => {
      const file = input.files[0];
      if (file.size > 1500000)
        return message.error('Image size exceeded 1.5Mb');
      setLoading({ image: true });
      const formData = new FormData();
      formData.append('image', file);
      const fileName = file.name;

      const imgUrl = await uploadFiles(file, quillRef);

      const range = quillRef.getSelection();

      quillRef.insertEmbed(range.index, 'image', imgUrl, 'user');
      let existingDelta = quillRef.getContents();
      const indexOf = existingDelta.ops.findIndex((eachOps) => {
        return eachOps.insert?.image === imgUrl;
      });
      const selectedOps = existingDelta.ops[indexOf];

      if (indexOf !== -1) {
        selectedOps.attributes = {};
        selectedOps.attributes = { alt: fileName };
      }

      quillRef.setContents(existingDelta);
      setLoading({ image: false });
    };
  };

  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          [{ header: [1, 2, 3, 4, 5, 6, false] }],
          ['bold', 'italic', 'underline'],
          [{ list: 'ordered' }, { list: 'bullet' }],
          [{ align: [] }],
          ['link', 'image'],
          ['clean'],
          [{ color: [] }]
        ],
        handlers: {
          image: imageHandler
        }
      },
      imageResize: {
        modules: ['Resize', 'DisplaySize']
      }
    }),
    []
  );

  return (
    <div className="react-quill-wrapper">
      <ReactQuill
        readOnly={readOnly}
        theme={readOnly ? 'bubble' : 'snow'}
        ref={(e) => {
          reactQuillRef = e;
        }}
        value={editorState}
        modules={modules}
        placeholder="Add content of your article!"
        onChange={onChange}
      />
    </div>
  );
};

export { RichTextEditor };

父组件调用代码

const [editorState, setEditorState] = useState('');

<RichTextEditor 
   editorState={editorState} 
   onChange={setEditorState} 
   setLoading={setLoading} 
/>

尝试思路

识别用户输入/粘贴的社交媒体链接,判断所属平台后使用react-social-media-embed生成嵌入内容,但该库输出为JSX,需转换为HTML字符串。

解决方案

步骤1:注册自定义HTML嵌入格式

Quill默认不支持直接插入HTML嵌入内容,需自定义一个Blot类型:

// 在组件顶部添加,Quill.register之前
const Embed = Quill.import('blots/embed');
class HtmlEmbed extends Embed {
  static create(value) {
    const node = super.create(value);
    node.innerHTML = value;
    node.setAttribute('contenteditable', 'false'); // 设置不可编辑
    node.style.margin = '10px 0';
    return node;
  }
  
  static value(node) {
    return node.innerHTML;
  }
}
HtmlEmbed.blotName = 'html';
HtmlEmbed.tagName = 'div';
Quill.register(HtmlEmbed);

步骤2:添加社交媒体链接检测函数

用于判断输入的文本是否为目标平台链接:

const isSocialMediaLink = (url) => {
  // 可扩展支持更多平台(Instagram、YouTube等)
  const tiktokRegex = /^https?:\/\/(www\.)?tiktok\.com\/@[\w-]+\/video\/\d+/;
  return tiktokRegex.test(url.trim());
};

步骤3:处理输入/粘贴的链接

在组件中添加监听事件,自动将链接替换为嵌入内容:

useEffect(() => {
  if (!quillRef) return;

  // 处理粘贴事件
  const handlePaste = (event) => {
    const clipboardData = event.clipboardData || window.clipboardData;
    const text = clipboardData.getData('text').trim();
    if (isSocialMediaLink(text)) {
      event.preventDefault();
      // 生成TikTok嵌入iframe
      const videoIdMatch = text.match(/video\/(\d+)/);
      if (videoIdMatch) {
        const videoId = videoIdMatch[1];
        const embedHtml = `<iframe src="https://www.tiktok.com/embed/v2/${videoId}" width="100%" height="480" frameborder="0" allowfullscreen></iframe>`;
        const range = quillRef.getSelection();
        quillRef.insertEmbed(range.index, 'html', embedHtml);
      }
    }
  };

  // 处理文本输入事件,自动转换单独成行的链接
  const handleTextChange = () => {
    const content = quillRef.getContents();
    const updatedOps = [...content.ops];
    let hasUpdates = false;

    updatedOps.forEach((op, index) => {
      if (op.insert && typeof op.insert === 'string') {
        const lines = op.insert.split('\n');
        const newLines = [];

        lines.forEach(line => {
          const trimmedLine = line.trim();
          if (isSocialMediaLink(trimmedLine)) {
            const videoIdMatch = trimmedLine.match(/video\/(\d+)/);
            if (videoIdMatch) {
              const videoId = videoIdMatch[1];
              const embedHtml = `<iframe src="https://www.tiktok.com/embed/v2/${videoId}" width="100%" height="480" frameborder="0" allowfullscreen></iframe>`;
              // 替换链接为嵌入内容
              newLines.push({ insert: '\n' });
              newLines.push({ insert: { html: embedHtml } });
              newLines.push({ insert: '\n' });
              hasUpdates = true;
            } else {
              newLines.push({ insert: line + '\n' });
            }
          } else {
            newLines.push({ insert: line + '\n' });
          }
        });

        // 移除最后多余的换行
        if (newLines.length && newLines[newLines.length - 1].insert === '\n') {
          newLines.pop();
        }

        if (hasUpdates) {
          updatedOps.splice(index, 1, ...newLines);
        }
      }
    });

    if (hasUpdates) {
      quillRef.setContents({ ops: updatedOps }, 'silent');
      onChange(quillRef.root.innerHTML);
    }
  };

  quillRef.root.addEventListener('paste', handlePaste);
  quillRef.on('text-change', handleTextChange);

  return () => {
    quillRef.root.removeEventListener('paste', handlePaste);
    quillRef.off('text-change', handleTextChange);
  };
}, [quillRef, onChange]);

步骤4:调整编辑器内容同步逻辑

确保onChange回调始终返回HTML字符串,上述代码中已通过quillRef.root.innerHTML直接传递最终的HTML内容。

说明

  • 该方案直接使用平台官方的嵌入iframe,无需依赖react-social-media-embed库,避免JSX转HTML的麻烦
  • 可通过扩展isSocialMediaLink中的正则表达式,支持更多社交媒体平台(如Instagram Reels、YouTube等)
  • 嵌入内容设置为不可编辑,避免用户误操作

内容的提问来源于stack exchange,提问作者Arian Nurrahman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 20:46:35