GrapesJS集成Tiptap富文本编辑器遇问题求解决方案
求问:是否有开发者成功将Tiptap集成为GrapesJS的富文本编辑器?
GrapesJS官方提供的自定义RTE集成示例极少,我在尝试将Tiptap作为它的富文本编辑器时,遇到了样式/继承冲突、换行异常、换行文本随机加粗等问题。希望能获取相关的集成经验或代码示例,以下是我目前尝试的代码:
import { Editor, Node } from "@tiptap/core"; import type { Editor as GrapesEditor } from "grapesjs"; import { TextStyle, FontFamily, FontSize, LineHeight } from "@tiptap/extension-text-style"; import Underline from "@tiptap/extension-underline"; import Document from "@tiptap/extension-document"; import Paragraph from "@tiptap/extension-paragraph"; import TextAlign from "@tiptap/extension-text-align"; import Heading from "@tiptap/extension-heading"; import Text from "@tiptap/extension-text"; import Bold from "@tiptap/extension-bold"; import Italic from "@tiptap/extension-italic"; import Strike from "@tiptap/extension-strike"; import Color from "@tiptap/extension-color"; import { ListKit } from "@tiptap/extension-list"; import { Token } from "../extensions/token"; function removeEmptyPTags(htmlContent: string = ""): string { // 移除无内容的p标签(包括带style等属性的) return htmlContent?.replace(/<p[^>]*>\s*<\/p>/gi, ''); } function stringReplacement(htmlContent: string = "") { const firstResult = htmlContent?.replace(/^\s*<p>\s*([\s\S]*?)\s*<\/p>\s*$/i, '$1'); const secondResult = firstResult?.replace(/<p>\s*<\/p>/gi, '<br/>'); const thirdResult = secondResult?.replace("<br/>", ''); return thirdResult; } function propagateStyles(el: HTMLElement, htmlContent: string): string { // 获取父元素的计算样式 const parentComputedStyle = window?.getComputedStyle(el); // 提取需要继承的文本样式 const stylesToPropagate = { fontFamily: parentComputedStyle?.fontFamily, fontSize: parentComputedStyle?.fontSize, fontWeight: parentComputedStyle?.fontWeight, fontStyle: parentComputedStyle?.fontStyle, color: parentComputedStyle?.color, lineHeight: parentComputedStyle?.lineHeight, letterSpacing: parentComputedStyle?.letterSpacing, // textAlign: parentComputedStyle.textAlign, textDecoration: parentComputedStyle?.textDecoration, }; // 创建临时容器解析HTML const tempDiv = document?.createElement('div'); tempDiv.innerHTML = htmlContent; // 为所有p、div、span元素应用父元素样式(仅当元素本身无该样式时) const elements = tempDiv?.querySelectorAll('p, div, span'); elements?.forEach((el) => { const element = el as HTMLElement; Object.entries(stylesToPropagate)?.forEach(([key, value]) => { if (value && value !== 'none') { // 将驼峰命名转为短横线命名的CSS属性 const cssKey = key?.replace(/([A-Z])/g, '-$1')?.toLowerCase(); if (!element?.style?.getPropertyValue(cssKey)) { element?.style?.setProperty(cssKey, value); } } }); }); return tempDiv?.innerHTML; } const CustomParagraph = Paragraph.extend({ addAttributes() { return { id: { default: null, parseHTML: element => element.getAttribute('id'), renderHTML: attributes => { if (!attributes.id) return {} return { id: attributes.id } }, }, class: { default: null, parseHTML: element => element.getAttribute('class'), renderHTML: attributes => { if (!attributes.class) return {} return { class: attributes.class } }, }, style: { default: null, parseHTML: element => element.getAttribute('style'), renderHTML: attributes => { if (!attributes.style) return {} return { style: attributes.style } }, }, } }, }); const CustomHeading = Heading.extend({ addAttributes() { return { id: { default: null, parseHTML: element => element.getAttribute('id'), renderHTML: attributes => { if (!attributes.id) return {} return { id: attributes.id } }, }, class: { default: null, parseHTML: element => element.getAttribute('class'), renderHTML: attributes => { if (!attributes.class) return {} return { class: attributes.class } }, }, style: { default: null, parseHTML: element => element.getAttribute('style'), renderHTML: attributes => { if (!attributes.style) return {} return { style: attributes.style } }, }, } }, }); const CustomDiv = Node.create({ name: "customDiv", group: "block", // 将div视为块级元素 content: "inline*", // 仅允许行内元素在div内(不允许p标签) // content: 'block*', parseHTML() { return [ { tag: "div", }, ]; }, renderHTML({ HTMLAttributes }) { return ["div", HTMLAttributes, 0]; }, }); export const TipTapPlugin = (editor: GrapesEditor, options: { rteInstance: Editor, setRteInstance: (instance: any) => void }) => { const { setRteInstance } = options; editor.setCustomRte({ // parseContent: true, // 此配置会引发问题 focus: (el: HTMLElement, rte: Editor) => { // 如果已聚焦则不处理 if (rte && rte?.isFocused) { return; } el.setAttribute("contenteditable", "true"); rte && rte.commands.focus("all", { scrollIntoView: true }); }, enable: (el, rte: Editor | null) => { // 清理已存在的编辑器实例 if (rte) { try { rte.destroy(); setRteInstance(null); } catch (e) { console.warn(e); } } console.log("TIPTAP enable", { el, rte }) // 提取内容,处理页面重载后已存在的ProseMirror内容 const content = el.querySelector("div.ProseMirror > p")?.innerHTML || el.innerHTML; const currentHTML = content; const secondResult = removeEmptyPTags(currentHTML); // 移除所有<br>标签(包括<br/>和<br />) const cleanedHTML = secondResult?.replace("<br/>", ""); el.innerHTML = ""; // 清空元素避免内容重复 // 初始化TipTap编辑器 rte = new Editor({ element: el, extensions: [ Document, CustomDiv, CustomParagraph, Text, // CustomTextStyle, // CustomSpan, TextStyle, CustomHeading, Bold, Italic, Strike, Underline, Color, FontFamily, FontSize, LineHeight, ListKit, TextAlign.configure({ types: ["heading", "paragraph"] }), Token, ], content: content, injectCSS: false, onCreate: ({ editor }) => { const currentHTML = editor?.getHTML(); const secondResult = removeEmptyPTags(currentHTML); // 移除所有<br>标签(包括<br/>和<br />) const cleanedHTML = secondResult?.replace("<br/>", ""); editor.commands.setContent(cleanedHTML); console.log("TIPTAP onCreate", { cleanedHTML, editor }) editor.commands.focus("all", { scrollIntoView: true }); // 不使用requestAnimationFrame时工作正常 // requestAnimationFrame(() => { // editor.commands.focus(); // editor.commands.focus("all", { scrollIntoView: true }); // }); // 获取当前HTML内容 } }); setRteInstance(rte); return rte; }, disable: (el, rte: Editor | null) => { console.log("TIPTAP disable", { el, rte }) el.setAttribute("contenteditable", "false"); // const htmlContent = rte?.getHTML() || ""; // const contentWithoutWrapper = stringReplacement(htmlContent) || el.innerHTML; // const contentWithStyles = propagateStyles(el, contentWithoutWrapper); rte?.commands?.blur(); rte?.destroy(); setRteInstance(null); // el.innerHTML = contentWithStyles; // return rte; }, getContent: (el, rte: Editor | null) => { console.log("TIPTAP getContent", { el, rte }) // 此逻辑会引发问题 const htmlContent = rte?.getHTML() || ""; el.innerHTML = htmlContent; const contentWithoutWrapper = stringReplacement(htmlContent) || el.innerHTML; const contentWithStyles = propagateStyles(el, contentWithoutWrapper); el.innerHTML = contentWithStyles; return el.innerHTML; } }); };
内容的提问来源于stack exchange,提问作者Abhinav Thapa
相关产品推荐
相关产品推荐

