在Tiptap编辑器中插入非HTML元素的方法(React):

huangapple go评论132阅读模式
英文:

Insert non-html elements in tiptap editor react

问题

我正在尝试在React.js中使用Tiptap编辑器,并结合使用一个名为"better-react-mathjax"的MathJax库,该库将LaTeX代码转换为可读的公式。

我的问题是该库将LaTeX代码转换后,将其包装在非HTML元素节点中,然后将其作为结果返回。但将该内容插入到Tiptap编辑器时,编辑器会将非HTML元素节点从中删除,导致公式显示出现问题。

我应该如何将非HTML元素节点插入到Tiptap编辑器中,以便使用Tiptap编辑器的默认功能来显示公式,同时覆盖Tiptap编辑器的此条件行为?

因为我对React.js和Tiptap编辑器都不太了解,所以对于任何帮助都表示感激。

示例:
我有一个类似下面这样的节点,我想将其添加到段落中。

<span style="display: block;"><mjx-container class="MathJax CtxtMenu_Attached_0" jax="CHTML" tabindex="0" ctxtmenu_counter="0" style="font-size: 103.9%; position: relative;"><mjx-math class="MJX-TEX" aria-hidden="true"><mjx-mfrac><mjx-frac><mjx-num><mjx-nstrut></mjx-nstrut><mjx-mn class="mjx-n" size="s"><mjx-c class="mjx-c31"></mjx-c><mjx-c class="mjx-c30"></mjx-c></mjx-mn></mjx-num><mjx-dbox><mjx-dtable><mjx-line></mjx-line><mjx-row><mjx-den><mjx-dstrut></mjx-dstrut><mjx-mrow size="s"><mjx-mn class="mjx-n"><mjx-c class="mjx-c34"></mjx-c></mjx-mn><mjx-mi class="mjx-i"><mjx-c class="mjx-c1D465 TEX-I"></mjx-c></mjx-mi></mjx-mrow></mjx-den></mjx-row></mjx-dtable></mjx-dbox></mjx-frac></mjx-mfrac><mjx-mo class="mjx-n" space="4"><mjx-c class="mjx-c2248"></mjx-c></mjx-mo><mjx-msup space="4"><mjx-mn class="mjx-n"><mjx-c class="mjx-c32"></mjx-c></mjx-mn><mjx-script style="vertical-align: 0.363em;"><mjx-texatom size="s" texclass="ORD"><mjx-mn class="mjx-n"><mjx-c class="mjx-c31"></mjx-c><mjx-c class="mjx-c32"></mjx-c></mjx-mn></mjx-texatom></mjx-script></mjx-msup></mjx-math><mjx-assistive-mml unselectable="on" display="inline"><mjx-container class="MathJax CtxtMenu_Attached_0" jax="CHTML" tabindex="0" ctxtmenu_counter="1" style="font-size: 103.9%; position: relative;"><mjx-math class="MJX-TEX" aria-hidden="true"><mjx-mfrac><mjx-frac><mjx-num><mjx-nstrut></mjx-nstrut><mjx-mn class="mjx-n" size="s"><mjx-c class="mjx-c31"></mjx-c><mjx-c class="mjx-c30"></mjx-c></mjx-mn></mjx-num><mjx-dbox><mjx-dtable><mjx-line></mjx-line><mjx-row><mjx-den><mjx-dstrut></mjx-dstrut><mjx-mrow size="s"><mjx-mn class="mjx-n"><mjx-c class="mjx-c34"></mjx-c></mjx-mn><mjx-mi class="mjx-i"><mjx-c class="mjx-c1D465 TEX-I"></mjx-c></mjx-mi></mjx-mrow></mjx-den></mjx-row></mjx-dtable></mjx-dbox></mjx-frac></mjx-mfrac><mjx-mo class="mjx-n" space="4"><mjx-c class="mjx-c2248"></mjx-c></mjx-mo><mjx-msup space="4"><mjx-mn class="mjx-n"><mjx-c class="mjx-c32"></mjx-c></mjx-mn><mjx-script style="vertical-align: 0.363em;"><mjx-texatom size="s" texclass="ORD"><mjx-mn class="mjx-n"><mjx-c class="mjx-c31"></mjx-c><mjx-c class="mjx-c32"></mjx-c></mjx-mn></mjx-texatom></mjx-script></mjx-msup></mjx-math></mjx-assistive-mml></mjx-container></mjx-assistive-mml></mjx-container></span>

但Tiptap编辑器会删除所有非HTML元素节点,如mjx-container、math等,只会在段落中追加span,结果如下:

在Tiptap编辑器中插入非HTML元素的方法(React):

英文:

I am trying to implement tiptap editor in react js with a mathjax library called better-react-mathjax which converts latex code to readable equations.

My issue is that the library converts the latex code and wrap it inside an non-html element node and then gives it as the result. But on inserting that content to tiptap editor, the editor removes the non-html element node from it. As a result the equation view breaks.

How I can insert non-html element node to tiptap editor to show the equation in format using the default functions by overriding this conditional behaviour of the tiptap editor.

Any help would be thankful as I am new to reactjs and also with this tiptap editor

Example:
I have a node like below which I want to add inside a paragraph.

&lt;span style=&quot;display: block;&quot;&gt;&lt;mjx-container class=&quot;MathJax CtxtMenu_Attached_0&quot; jax=&quot;CHTML&quot; tabindex=&quot;0&quot; ctxtmenu_counter=&quot;0&quot; style=&quot;font-size: 103.9%; position: relative;&quot;&gt;&lt;mjx-math class=&quot;MJX-TEX&quot; aria-hidden=&quot;true&quot;&gt;&lt;mjx-mfrac&gt;&lt;mjx-frac&gt;&lt;mjx-num&gt;&lt;mjx-nstrut&gt;&lt;/mjx-nstrut&gt;&lt;mjx-mn class=&quot;mjx-n&quot; size=&quot;s&quot;&gt;&lt;mjx-c class=&quot;mjx-c31&quot;&gt;&lt;/mjx-c&gt;&lt;mjx-c class=&quot;mjx-c30&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;/mjx-num&gt;&lt;mjx-dbox&gt;&lt;mjx-dtable&gt;&lt;mjx-line&gt;&lt;/mjx-line&gt;&lt;mjx-row&gt;&lt;mjx-den&gt;&lt;mjx-dstrut&gt;&lt;/mjx-dstrut&gt;&lt;mjx-mrow size=&quot;s&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c34&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;mjx-mi class=&quot;mjx-i&quot;&gt;&lt;mjx-c class=&quot;mjx-c1D465 TEX-I&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mi&gt;&lt;/mjx-mrow&gt;&lt;/mjx-den&gt;&lt;/mjx-row&gt;&lt;/mjx-dtable&gt;&lt;/mjx-dbox&gt;&lt;/mjx-frac&gt;&lt;/mjx-mfrac&gt;&lt;mjx-mo class=&quot;mjx-n&quot; space=&quot;4&quot;&gt;&lt;mjx-c class=&quot;mjx-c2248&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mo&gt;&lt;mjx-msup space=&quot;4&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c32&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;mjx-script style=&quot;vertical-align: 0.363em;&quot;&gt;&lt;mjx-texatom size=&quot;s&quot; texclass=&quot;ORD&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c31&quot;&gt;&lt;/mjx-c&gt;&lt;mjx-c class=&quot;mjx-c32&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;/mjx-texatom&gt;&lt;/mjx-script&gt;&lt;/mjx-msup&gt;&lt;/mjx-math&gt;&lt;mjx-assistive-mml unselectable=&quot;on&quot; display=&quot;inline&quot;&gt;&lt;mjx-container class=&quot;MathJax CtxtMenu_Attached_0&quot; jax=&quot;CHTML&quot; tabindex=&quot;0&quot; ctxtmenu_counter=&quot;1&quot; style=&quot;font-size: 103.9%; position: relative;&quot;&gt;&lt;mjx-math class=&quot;MJX-TEX&quot; aria-hidden=&quot;true&quot;&gt;&lt;mjx-mfrac&gt;&lt;mjx-frac&gt;&lt;mjx-num&gt;&lt;mjx-nstrut&gt;&lt;/mjx-nstrut&gt;&lt;mjx-mn class=&quot;mjx-n&quot; size=&quot;s&quot;&gt;&lt;mjx-c class=&quot;mjx-c31&quot;&gt;&lt;/mjx-c&gt;&lt;mjx-c class=&quot;mjx-c30&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;/mjx-num&gt;&lt;mjx-dbox&gt;&lt;mjx-dtable&gt;&lt;mjx-line&gt;&lt;/mjx-line&gt;&lt;mjx-row&gt;&lt;mjx-den&gt;&lt;mjx-dstrut&gt;&lt;/mjx-dstrut&gt;&lt;mjx-mrow size=&quot;s&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c34&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;mjx-mi class=&quot;mjx-i&quot;&gt;&lt;mjx-c class=&quot;mjx-c1D465 TEX-I&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mi&gt;&lt;/mjx-mrow&gt;&lt;/mjx-den&gt;&lt;/mjx-row&gt;&lt;/mjx-dtable&gt;&lt;/mjx-dbox&gt;&lt;/mjx-frac&gt;&lt;/mjx-mfrac&gt;&lt;mjx-mo class=&quot;mjx-n&quot; space=&quot;4&quot;&gt;&lt;mjx-c class=&quot;mjx-c2248&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mo&gt;&lt;mjx-msup space=&quot;4&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c32&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;mjx-script style=&quot;vertical-align: 0.363em;&quot;&gt;&lt;mjx-texatom size=&quot;s&quot; texclass=&quot;ORD&quot;&gt;&lt;mjx-mn class=&quot;mjx-n&quot;&gt;&lt;mjx-c class=&quot;mjx-c31&quot;&gt;&lt;/mjx-c&gt;&lt;mjx-c class=&quot;mjx-c32&quot;&gt;&lt;/mjx-c&gt;&lt;/mjx-mn&gt;&lt;/mjx-texatom&gt;&lt;/mjx-script&gt;&lt;/mjx-msup&gt;&lt;/mjx-math&gt;&lt;mjx-assistive-mml unselectable=&quot;on&quot; display=&quot;inline&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;mfrac&gt;&lt;mn&gt;10&lt;/mn&gt;&lt;mrow&gt;&lt;mn&gt;4&lt;/mn&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;mo&gt;≈&lt;/mo&gt;&lt;msup&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mrow data-mjx-texclass=&quot;ORD&quot;&gt;&lt;mn&gt;12&lt;/mn&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/math&gt;&lt;/mjx-assistive-mml&gt;&lt;/mjx-container&gt;&lt;/mjx-assistive-mml&gt;&lt;/mjx-container&gt;&lt;/span&gt;

But tiptap editor removes all the non-html element nodes like mjx-container, math etc and only appends span inside the paragraph

Like below

在Tiptap编辑器中插入非HTML元素的方法(React):

答案1

得分: 2

TLDR: 创建您自己的扩展或尝试使用一个ProseMirror扩展prosemirror-math extension(或将其用作灵感)

Tiptap将解析您提供的HTML作为初始内容,并将其转换为您在编辑器中使用的扩展。正如您所说,如果有HTML无法解析为任何扩展,这些部分将被删除。

在您的情况下,您需要创建自定义扩展(可能有多个)
示例在此
文档在此

这里重要的是:

parseHTML:Tiptap应该如何解析HTML以适应您的扩展。我建议创建您自己的标签,例如<math>,但您也可以使用常规HTML标记并使用属性进行正确解析。

renderHTML:Tiptap应该如何将您的扩展返回到“html”中,这在您想要从编辑器导出HTML到存储时经常使用。

addNodeView:这是魔法发生的地方,因为您可以使用自己的React组件在编辑器中显示。

总之,parseHTML是Tiptap不应该使用您的扩展的方式。renderHTML用于将内容存储在外部存储中。addNodeView是您使用React组件的地方。

我没有数学方程的示例,但这是我使用的自定义扩展的一些内容,您可以将其视为模板:

import {
  ActionIcon,
  Box,
  Checkbox,
  createStyles,
  Modal,
  Text,
} from "@mantine/core";
import { Editor, NodeViewWrapper } from "@tiptap/react";
import React, { useState } from "react";

import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { useTranslation } from "react-i18next";
import ModalButtonGroup from "src/components/modal/ModalButtonGroup";
import { AddressBook as AddressBookIcon } from "tabler-icons-react";

export interface ContactsInputOptions {
  contacts?: string[];
}
declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    addressBook: {
      insertAddressBook: () => ReturnType;
    };
  }
}
const AddressBook = Node.create<ContactsInputOptions>({
  name: "addressBook",
  inline: true,
  group: "inline",

  addOptions() {
    return {
      ...this.parent?.(),
    };
  },

  addCommands() {
    return {
      insertAddressBook:
        () =>
        ({ commands }) => {
          return commands.insertContent({
            type: "addressBook",
          });
        },
    };
  },
  parseHTML() {
    return [
      {
        tag: "address-book",
      },
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return ["address-book", mergeAttributes(HTMLAttributes)];
  },

  addNodeView() {
    return ReactNodeViewRenderer(AddressBookComponent);
  },
});

const useStyles = createStyles((theme) => ({
  inputWrapper: {
    display: "inline-block",
    verticalAlign: "bottom",
  },
}));

interface AddressBookComponentProps {
  node: { attrs: any };
  updateAttributes: (attrs: any) => void;
  selected: boolean;
  editor: Editor;
  getPos: () => number;
  extension: Node;
}

const AddressBookComponent: React.FC<AddressBookComponentProps> = ({
  editor,
  node,
  updateAttributes,
  getPos,
  extension,
}) => {
  const { classes } = useStyles();
  const [opened, setOpened] = useState(false);
  if (!editor isEditable) {
    return null;
  }
  return (
    <NodeViewWrapper className={classes.inputWrapper}>
      {opened && (
        <ContactsModal
          onClose={() => setOpened(false)}
          onAdd={(text) => {
            editor.chain().focus().insertContentAt(getPos(), text).run();
            setOpened(false);
          }}
          contacts={extension.options.contacts}
        />
      )}
      <ActionIcon
        size={"sm"}
        onClick={() => {
          return setOpened(true);
        }}
      >
        <AddressBookIcon />
      </ActionIcon>
    </NodeViewWrapper>
  );
};

const ContactsModal: React.FC<{
  onClose: () => void;
  onAdd: (value: string) => void;
  contacts?: string[];
}> = ({ contacts, onClose, onAdd }) => {
  const { t } = useTranslation("notepad", { keyPrefix: "addressBook" });
  const [selectedContacts, setSelectedContacts] = useState<number[]>([]);
  return (
    <Modal opened={true} onClose={() => onClose()} title={t("modalTitle")}>
      {contacts && contacts.length > 0 ? (
        contacts.map((contact, index) => {
          return (
            <Box key={contact} pt={"sm"}>
              <Checkbox
                label={contact}
                checked={selectedContacts.includes(index)}
                onChange={(event) => {
                  if (event.target.checked) {
                    setSelectedContacts([...selectedContacts, index]);
                  } else {
                    setSelectedContacts(
                      selectedContacts.filter((i) => i !== index)
                    );
                  }
                }}
              />
            </Box>
          );
        })
      ) : (
        <Text>{t("noContactsFound")}</Text>
      )}
      <ModalButtonGroup
        onCancel={() => onClose()}
        onSubmit={() => {
          contacts &&
            onAdd(
              contacts
                ?.filter((_, index) => selectedContacts.includes(index))
                .join(", ")
            );
        }}
        submitLabel={t("insert")}
        disabled={selectedContacts.length === 0}
      />
    </Modal>
  );
};

export default AddressBook;

这是您提供的内容的翻译。

英文:

TLDR: Create your own extension or try to use a prosemirror extensionprosemirror-math extension (Or use it as a inspiration)

Tiptap will parse the html that you provide as initial content and convert it to the extensions that you use in your editor. As you say if there is html that cant be parsed to any extension this parts will be remove.

In your case you will need to create a custom extension (probably several)
Example here
Documentation here

Whats important here is:

parseHTML: How tiptap should parse html to your extension. I would recommend to create your own tag e.g. <math>, however you could use regular html tags and use attributes to parse it correctly.

renderHTML: How tiptap should return your extension to "html" here you use your own tag names. Mostly used when you want to export html from the editor to storage.

addNodeView: Here is where the magic happens, since you can use your own react components to show in the editor.

In summary parseHTMLhow tiptap should not to use your extension. renderHTML to be able to store your content in external storage. addNodeView here is where you use react components.

I do not have a example with math equations, however this is a custom extension I use that you could see as some of a template:

import {
  ActionIcon,
  Box,
  Checkbox,
  createStyles,
  Modal,
  Text,
} from &quot;@mantine/core&quot;;
import { Editor, NodeViewWrapper } from &quot;@tiptap/react&quot;;
import React, { useState } from &quot;react&quot;;

import { mergeAttributes, Node } from &quot;@tiptap/core&quot;;
import { ReactNodeViewRenderer } from &quot;@tiptap/react&quot;;
import { useTranslation } from &quot;react-i18next&quot;;
import ModalButtonGroup from &quot;src/components/modal/ModalButtonGroup&quot;;
import { AddressBook as AddressBookIcon } from &quot;tabler-icons-react&quot;;

export interface ContactsInputOptions {
  contacts?: string[];
}
declare module &quot;@tiptap/core&quot; {
  interface Commands&lt;ReturnType&gt; {
    addressBook: {
      insertAddressBook: () =&gt; ReturnType;
    };
  }
}
const AddressBook = Node.create&lt;ContactsInputOptions&gt;({
  name: &quot;addressBook&quot;,
  inline: true,
  group: &quot;inline&quot;,

  addOptions() {
    return {
      ...this.parent?.(),
    };
  },

  addCommands() {
    return {
      insertAddressBook:
        () =&gt;
        ({ commands }) =&gt; {
          return commands.insertContent({
            type: &quot;addressBook&quot;,
          });
        },
    };
  },
  parseHTML() {
    return [
      {
        tag: &quot;address-book&quot;,
      },
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return [&quot;address-book&quot;, mergeAttributes(HTMLAttributes)];
  },

  addNodeView() {
    return ReactNodeViewRenderer(AddressBookComponent);
  },
});

const useStyles = createStyles((theme) =&gt; ({
  inputWrapper: {
    display: &quot;inline-block&quot;,
    verticalAlign: &quot;bottom&quot;,
  },
}));

interface AddressBookComponentProps {
  node: { attrs: any };
  updateAttributes: (attrs: any) =&gt; void;
  selected: boolean;
  editor: Editor;
  getPos: () =&gt; number;
  extension: Node;
}

const AddressBookComponent: React.FC&lt;AddressBookComponentProps&gt; = ({
  editor,
  node,
  updateAttributes,
  getPos,
  extension,
}) =&gt; {
  const { classes } = useStyles();
  const [opened, setOpened] = useState(false);
  if (!editor.isEditable) {
    return null;
  }
  return (
    &lt;NodeViewWrapper className={classes.inputWrapper}&gt;
      {opened &amp;&amp; (
        &lt;ContactsModal
          onClose={() =&gt; setOpened(false)}
          onAdd={(text) =&gt; {
            editor.chain().focus().insertContentAt(getPos(), text).run();
            setOpened(false);
          }}
          contacts={extension.options.contacts}
        /&gt;
      )}
      &lt;ActionIcon
        size={&quot;sm&quot;}
        onClick={() =&gt; {
          return setOpened(true);
        }}
      &gt;
        &lt;AddressBookIcon /&gt;
      &lt;/ActionIcon&gt;
    &lt;/NodeViewWrapper&gt;
  );
};

const ContactsModal: React.FC&lt;{
  onClose: () =&gt; void;
  onAdd: (value: string) =&gt; void;
  contacts?: string[];
}&gt; = ({ contacts, onClose, onAdd }) =&gt; {
  const { t } = useTranslation(&quot;notepad&quot;, { keyPrefix: &quot;addressBook&quot; });
  const [selectedContacts, setSelectedContacts] = useState&lt;number[]&gt;([]);
  return (
    &lt;Modal opened={true} onClose={() =&gt; onClose()} title={t(&quot;modalTitle&quot;)}&gt;
      {contacts &amp;&amp; contacts.length &gt; 0 ? (
        contacts.map((contact, index) =&gt; {
          return (
            &lt;Box key={contact} pt={&quot;sm&quot;}&gt;
              &lt;Checkbox
                label={contact}
                checked={selectedContacts.includes(index)}
                onChange={(event) =&gt; {
                  if (event.target.checked) {
                    setSelectedContacts([...selectedContacts, index]);
                  } else {
                    setSelectedContacts(
                      selectedContacts.filter((i) =&gt; i !== index)
                    );
                  }
                }}
              /&gt;
            &lt;/Box&gt;
          );
        })
      ) : (
        &lt;Text&gt;{t(&quot;noContactsFound&quot;)}&lt;/Text&gt;
      )}
      &lt;ModalButtonGroup
        onCancel={() =&gt; onClose()}
        onSubmit={() =&gt; {
          contacts &amp;&amp;
            onAdd(
              contacts
                ?.filter((_, index) =&gt; selectedContacts.includes(index))
                .join(&quot;, &quot;)
            );
        }}
        submitLabel={t(&quot;insert&quot;)}
        disabled={selectedContacts.length === 0}
      /&gt;
    &lt;/Modal&gt;
  );
};

export default AddressBook;

huangapple
  • 本文由 发表于 2023年2月27日 17:42:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/75578831.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定