@rtdui/editor
Version:
React rich text editor based on tiptap
215 lines (213 loc) • 8.13 kB
JavaScript
'use client';
import { UploadImageWithResizable } from "./tiptap_extensions/extension-image-upload/uploadImageWithResizable.mjs";
import { MarkdownPaste } from "./tiptap_extensions/extension-markdown-paste/markdownPaste.mjs";
import { MathKatexInline } from "./tiptap_extensions/extension-math/math-katex-inline.mjs";
import { MathKatexBlock } from "./tiptap_extensions/extension-math/math-katex-block.mjs";
import { CodeBlockShiki } from "./tiptap_extensions/extension-code-block-shiki/code-block-shiki.mjs";
import { CharacterCount, Highlight, Placeholder, StarterKit, Subscript, Superscript, TableKit, TaskItem, TaskList, TextAlign, TextStyleKit } from "./tiptap.mjs";
import { Toolbar } from "./toolbar/Toolbar.mjs";
import "./toolbar/index.mjs";
import { useImperativeHandle } from "react";
import clsx from "clsx";
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
import { FloatingMenu } from "@tiptap/react/menus";
import { IconArrowBarBoth, IconBoxAlignLeft, IconBoxAlignTop, IconBoxAlignTopLeft, IconColumnInsertLeft, IconColumnInsertRight, IconContainerOff, IconRowInsertBottom, IconRowInsertTop, IconSquareToggle, IconTable, IconTableOff, IconViewportNarrow } from "@tabler/icons-react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
//#region packages/editor/src/RichTextEditor/RichTextEditor.tsx
function RichTextEditor(props) {
const { ref, slots, editable = true, placeholder = "输入内容或者从剪贴板粘贴, 复制粘贴支持Markdown", limit, uploadImageUrl, imageResizable = true, className, ...other } = props;
const editorOptions = {
extensions: [
StarterKit.configure({
codeBlock: false,
link: { openOnClick: !editable }
}),
TableKit.configure({
table: { resizable: true },
tableCell: { HTMLAttributes: { class: "not-prose" } },
tableHeader: { HTMLAttributes: { class: "not-prose" } }
}),
TaskItem.configure({ nested: true }),
TaskList,
Superscript,
Subscript,
Highlight,
Placeholder.configure({ placeholder }),
TextAlign.configure({ types: [
"heading",
"paragraph",
"tableHeader",
"tableCell"
] }),
TextStyleKit,
CodeBlockShiki,
UploadImageWithResizable.configure({
resizable: imageResizable,
inline: true,
url: uploadImageUrl,
method: "post"
}),
MarkdownPaste,
MathKatexInline,
MathKatexBlock
],
editable
};
if (limit) editorOptions.extensions?.push(CharacterCount.configure({ limit }));
const editor = useEditor(editorOptions);
const { charactersCount, wordsCount } = useEditorState({
editor,
selector: (context) => ({
charactersCount: context.editor?.storage?.characterCount?.characters(),
wordsCount: context.editor?.storage?.characterCount?.words()
})
});
useImperativeHandle(ref, () => ({
getJSON: () => {
if (editor) return editor.getJSON();
return null;
},
setContent: (jsonOrHtml) => {
if (editor) editor.commands.setContent(jsonOrHtml);
}
}));
if (!editor) return null;
return /* @__PURE__ */ jsxs("div", {
...other,
className: clsx(className),
children: [
editable && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Toolbar, {
editor,
className: clsx("editor-content", slots?.toolbar)
}), /* @__PURE__ */ jsx(FloatingMenu, {
editor,
shouldShow: ({ editor }) => editor.isActive("tableCell") || editor.isActive("tableHeader"),
options: { autoPlacement: { allowedPlacements: ["top-start", "top-end"] } },
children: /* @__PURE__ */ jsxs("div", {
className: "join",
children: [
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "插入表格",
onClick: () => editor.chain().focus().insertTable({
rows: 3,
cols: 3,
withHeaderRow: true
}).run(),
children: /* @__PURE__ */ jsx(IconTable, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "左侧插入列",
onClick: () => editor.chain().focus().addColumnBefore().run(),
children: /* @__PURE__ */ jsx(IconColumnInsertLeft, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "右侧插入列",
onClick: () => editor.chain().focus().addColumnAfter().run(),
children: /* @__PURE__ */ jsx(IconColumnInsertRight, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "删除列",
onClick: () => editor.chain().focus().deleteColumn().run(),
children: /* @__PURE__ */ jsx(IconContainerOff, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "上方插入行",
onClick: () => editor.chain().focus().addRowBefore().run(),
children: /* @__PURE__ */ jsx(IconRowInsertTop, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "下方插入行",
onClick: () => editor.chain().focus().addRowAfter().run(),
children: /* @__PURE__ */ jsx(IconRowInsertBottom, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "删除行",
onClick: () => editor.chain().focus().deleteRow().run(),
children: /* @__PURE__ */ jsx(IconContainerOff, {
className: "rotate-90",
stroke: 1
})
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "删除表格",
onClick: () => editor.chain().focus().deleteTable().run(),
children: /* @__PURE__ */ jsx(IconTableOff, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "合并单元格",
onClick: () => editor.chain().focus().mergeCells().run(),
children: /* @__PURE__ */ jsx(IconViewportNarrow, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "拆分单元格",
onClick: () => editor.chain().focus().splitCell().run(),
children: /* @__PURE__ */ jsx(IconArrowBarBoth, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "合并或拆分",
onClick: () => editor.chain().focus().mergeOrSplit().run(),
children: /* @__PURE__ */ jsx(IconSquareToggle, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "首列切换",
onClick: () => editor.chain().focus().toggleHeaderColumn().run(),
children: /* @__PURE__ */ jsx(IconBoxAlignLeft, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "首行切换",
onClick: () => editor.chain().focus().toggleHeaderRow().run(),
children: /* @__PURE__ */ jsx(IconBoxAlignTop, { stroke: 1 })
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: "join-item btn btn-sm px-1",
title: "单元格切换行头样式",
onClick: () => editor.chain().focus().toggleHeaderCell().run(),
children: /* @__PURE__ */ jsx(IconBoxAlignTopLeft, { stroke: 1 })
})
]
})
})] }),
/* @__PURE__ */ jsx(EditorContent, {
editor,
className: clsx("editor-content prose max-w-none! overflow-y-auto", slots?.content)
}),
limit && /* @__PURE__ */ jsxs("div", {
className: "character-count text-xs flex flex-row-reverse px-1.5",
children: [
charactersCount,
" / ",
limit
]
})
]
});
}
//#endregion
export { RichTextEditor };