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

React Portal新窗口中Quill编辑器工具栏未初始化问题求助

解决React Portal新窗口中Quill编辑器工具栏无响应问题

问题核心

通过React Portal在新窗口渲染Quill编辑器时,文本同步正常,但工具栏按钮点击无反应,原因是Quill默认将事件监听器绑定到主窗口的document对象,即使编辑器DOM在新窗口,点击事件仍会被发送到原窗口的document,导致新窗口的工具栏操作无法触发。

解决方案步骤

1. 修复NewWindowPortal的异步状态问题

原代码中使用setNewWindow异步更新状态,后续代码直接访问newWindow会拿到初始null值,导致DOM操作失效。改用useRef直接保存新窗口和容器实例:

export const NewWindowPortal = ({ onClose, children }: { onClose: () => void; children: (props) => ReactNode }) => {
  const ref = useRef(null);
  const containerRef = useRef<HTMLElement | null>(null);
  const newWindowRef = useRef<Window | null>(null);

  useEffect(() => {
    const newWindow = window.open("", "", "width=800,height=700,left=200,top=200");
    if (!newWindow) return;
    newWindowRef.current = newWindow;

    // 复制主窗口head资源到新窗口
    const parentHead = window.document.querySelector("head").childNodes;
    parentHead.forEach((item) => {
      if (item.nodeName === "TITLE") {
        newWindow.document.title = `${(item as HTMLElement).innerHTML} - begrunnelse`;
      } else {
        newWindow.document.head.appendChild(item.cloneNode(true));
      }
    });

    // 创建Portal容器并挂载到新窗口body
    const container = newWindow.document.createElement("div");
    containerRef.current = container;
    newWindow.document.body.appendChild(container);

    // 监听新窗口关闭事件
    const handleBeforeUnload = () => onClose();
    newWindow.addEventListener("beforeunload", handleBeforeUnload);

    return () => {
      newWindow.removeEventListener("beforeunload", handleBeforeUnload);
      newWindow.close();
      newWindowRef.current = null;
      containerRef.current = null;
    };
  }, [onClose]);

  return containerRef.current ? createPortal(
    children({ ref, newDocument: newWindowRef.current?.document }),
    containerRef.current
  ) : null;
};

2. 让Quill完全绑定到新窗口的document

修改CustomQuillEditor,确保所有DOM操作、事件监听都基于新窗口的document,并手动重绑定工具栏的点击事件:

export const CustomQuillEditor = ({ readOnly, defaultValue, onTextChange, ref, resize, newDocument }: EditorProps) => {
  const containerRef = useRef(null);
  const quill = useRef<Quill | null>(null);

  useEffect(() => {
    if (!containerRef.current || !newDocument) return;

    // 使用新窗口document创建编辑器容器
    const editorContainer = newDocument.createElement("div");
    const container = containerRef.current;
    container.appendChild(editorContainer);

    // 初始化Quill
    quill.current = new Quill(editorContainer, {
      theme: "snow",
      readOnly,
      modules: {
        history: {},
        toolbar: readOnly
          ? false
          : {
              container: [["bold", "italic", "underline", { header: 3 }]],
            },
      },
    });

    ref.current = quill.current;

    // 关键:将工具栏点击事件绑定到新窗口document
    const toolbar = quill.current.getModule("toolbar");
    if (toolbar && !readOnly) {
      const toolbarElement = toolbar.container;
      // 移除默认绑定到主document的事件
      toolbarElement.removeEventListener("click", toolbar.handler);
      // 重新绑定到新窗口document
      newDocument.addEventListener("click", toolbar.handler);
    }

    // 文本变更监听
    quill.current.on(Quill.events.TEXT_CHANGE, () => {
      if (!quill.current) return;
      if (quill.current.getLength() <= 1) {
        onTextChange("");
      } else {
        onTextChange(quill.current.getSemanticHTML().replaceAll("<p></p>", "<p><br/></p>"));
      }
    });

    return () => {
      ref.current = null;
      if (quill.current && newDocument) {
        const toolbar = quill.current.getModule("toolbar");
        if (toolbar) {
          newDocument.removeEventListener("click", toolbar.handler);
        }
        quill.current = null;
      }
      container.innerHTML = "";
    };
  }, [ref, newDocument, readOnly, onTextChange]);

  useEffect(() => {
    if (!quill.current) return;
    const currentHTML = quill.current.getSemanticHTML().replaceAll("<p></p>", "<p><br/></p>");
    if (defaultValue !== currentHTML) {
      const updatedDelta = quill.current.clipboard.convert({ html: defaultValue });
      quill.current.setContents(updatedDelta, "silent");
    }
  }, [defaultValue]);

  return (
    <div
      spellCheck={false}
      className={`ql-top-container ${readOnly ? "readonly" : ""} ${resize ? "resizable" : ""}`}
      ref={containerRef}
    ></div>
  );
};

3. 关键修改说明

  • 异步状态修复:用useRef直接保存实例,避免React state异步更新导致的DOM操作失效。
  • 事件重绑定:Quill工具栏默认将点击事件绑定到主窗口document,需手动移除并重新绑定到新窗口document,确保事件能被正确捕获。
  • 全链路使用新document:从容器创建到编辑器初始化,所有DOM操作都基于新窗口document,避免跨document的操作冲突。

验证效果

修改后,新窗口中的Quill工具栏点击事件会被正确绑定到新窗口document,按钮操作可正常触发,同时文本同步功能保持正常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:44:54