@explita/editor
Version:
`@explita/editor` is a versatile, modern rich-text editor built on TipTap for seamless integration into React applications. It provides extensive customization options and advanced features to cater to diverse content creation needs.
237 lines (236 loc) • 8.71 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import StarterKit from "@tiptap/starter-kit";
import { useEditor, EditorContent } from "@tiptap/react";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import Table from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row";
import Underline from "@tiptap/extension-underline";
import FontFamily from "@tiptap/extension-font-family";
import TextStyle from "@tiptap/extension-text-style";
import Superscript from "@tiptap/extension-superscript";
import Subscript from "@tiptap/extension-subscript";
import { Color } from "@tiptap/extension-color";
import Highlight from "@tiptap/extension-highlight";
import Link from "@tiptap/extension-link";
import TextAlign from "@tiptap/extension-text-align";
import BubbleMenu from "@tiptap/extension-bubble-menu";
import { ImageResize } from "tiptap-extension-resize-image";
import { useEditorStore } from "../store/useEditorState";
import { Ruler } from "./Ruler";
import { FontSize } from "../extensions/font-size";
import { LineHeight } from "../extensions/line-height";
import { PageBreak } from "../extensions/page-break";
import { INCH_TO_PX } from "../lib/constants";
import { LuLoader } from "react-icons/lu";
import CharacterCount from "@tiptap/extension-character-count";
import { PopupMenu } from "./PopupMenu";
import { useEffect, useState } from "react";
import { Toolbar } from "./Toolbar";
export function EditorInterface() {
const { setEditor, editorOpts: { padding, zoomLevel, editorHeight, editorWidth }, } = useEditorStore();
const editor = useEditor({
immediatelyRender: false,
onCreate({ editor }) {
setEditor(editor);
},
onDestroy() {
setEditor(null);
},
onUpdate: ({ editor }) => {
setEditor(editor);
},
onSelectionUpdate: ({ editor }) => {
setEditor(editor);
},
onTransaction: ({ editor }) => {
setEditor(editor);
},
onFocus: ({ editor }) => {
setEditor(editor);
},
onBlur: ({ editor }) => {
setEditor(editor);
},
onContentError: ({ editor }) => {
setEditor(editor);
},
editorProps: {
attributes: {
style: `padding-left:${(padding.left || 0) * INCH_TO_PX}px;
padding-right:${(padding.right || 0) * INCH_TO_PX}px; padding-top:${(padding.top || 0) * INCH_TO_PX}px;
padding-bottom:${(padding.bottom || 0) * INCH_TO_PX}px;
transform: scale(${zoomLevel}); transform-origin: top center;
width: ${editorWidth}; min-height: ${editorHeight};
`,
class: "editor-content",
},
},
extensions: [
StarterKit,
LineHeight.configure({
types: ["paragraph", "heading"],
}),
FontSize,
PageBreak,
CharacterCount,
BubbleMenu,
Superscript,
Subscript,
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: "https",
}),
Highlight.configure({ multicolor: true }),
TextStyle,
Color,
FontFamily,
Underline,
ImageResize,
TaskList,
TaskItem.configure({
nested: true,
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
content: "",
});
return (_jsx(_Fragment, { children: editor === null ? (_jsx("div", { className: "editor-loader", "data-mini": false, children: _jsx(LuLoader, {}) })) : (_jsxs("main", { className: "editor-content-wrapper", "data-mini": false, children: [_jsx(Ruler, {}, editorWidth), _jsx(PopupMenu, {}), _jsx(EditorContent, { editor: editor, "data-mini": false })] })) }));
}
/**
* A compact version of the Editor component, suitable for inline editing.
* Supports most features of the full editor, but with a smaller footprint.
* @param {{
* padding?: string;
* width?: string;
* height?: string;
* hideToolbar?: boolean;
* readOnly?: boolean;
* initialContent?: string | JSONContent;
* name?: string;
* id?: string;
* outputType?: "html" | "json" | "text";
* onValueChange?: (value: string) => void;
* }} props
* @returns {JSX.Element}
*/
export function CompactEditor({ padding = "10px", width = "100%", height = "300px", hideToolbar = false, readOnly = false, initialContent, name, id, outputType = "html", onValueChange, }) {
const { setEditor } = useEditorStore();
const editor = useEditor({
immediatelyRender: false,
onCreate({ editor }) {
setEditor(editor);
},
onDestroy() {
setEditor(null);
},
onUpdate: ({ editor }) => {
setEditor(editor);
},
onSelectionUpdate: ({ editor }) => {
setEditor(editor);
},
onTransaction: ({ editor }) => {
setEditor(editor);
},
onFocus: ({ editor }) => {
setEditor(editor);
},
onBlur: ({ editor }) => {
setEditor(editor);
},
onContentError: ({ editor }) => {
setEditor(editor);
},
editorProps: {
attributes: {
style: `padding:${padding};
width: 100%; min-height: calc(${height} - 45px);
`,
class: "editor-content",
},
},
extensions: [
StarterKit,
LineHeight.configure({
types: ["paragraph", "heading"],
}),
FontSize,
CharacterCount,
BubbleMenu,
Superscript,
Subscript,
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: "https",
}),
Highlight.configure({ multicolor: true }),
TextStyle,
Color,
FontFamily,
Underline,
ImageResize,
TaskList,
TaskItem.configure({
nested: true,
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
content: "",
});
const [output, setOutput] = useState("");
useEffect(() => {
if (editor) {
if (outputType === "html") {
setOutput(editor.getHTML());
}
else if (outputType === "json") {
setOutput(JSON.stringify(editor.getJSON()));
}
else if (outputType === "text") {
setOutput(editor.getText());
}
}
}, [editor?.getHTML(), editor?.getJSON(), editor?.getText(), outputType]);
useEffect(() => {
if (onValueChange) {
setTimeout(() => {
onValueChange(output);
}, 200);
}
}, [output]);
useEffect(() => {
if (editor &&
initialContent &&
(typeof initialContent === "object" || typeof initialContent === "string")) {
editor.commands.setContent(initialContent);
}
}, [editor, initialContent]);
useEffect(() => {
if (editor) {
editor.setEditable(!readOnly);
}
}, [editor, readOnly]);
return (_jsx("section", { className: "explita-editor", "data-mini": true, style: { width: width, maxHeight: height }, children: editor === null ? (_jsx("div", { className: "editor-loader", "data-mini": true, children: _jsx(LuLoader, {}) })) : (_jsxs(_Fragment, { children: [!hideToolbar && (_jsx("header", { className: "editor-header", "data-mini": true, "data-focused": editor?.isFocused, children: _jsx(Toolbar, { isMini: true }) })), _jsxs("main", { className: "editor-content-wrapper", "data-mini": true, children: [_jsx(EditorContent, { editor: editor, "data-mini": true }), _jsx("input", { type: "hidden", name: name, id: id, value: output })] })] })) }));
}