import React, { forwardRef, useCallback, useEffect, useRef } from 'react'; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; import Image from '@tiptap/extension-image'; import Placeholder from '@tiptap/extension-placeholder'; import { FormControl, FormHelperText, FormLabel, useTheme, IconButton, Box } from '@mui/material'; import { FormatBold, FormatItalic, StrikethroughS, FormatListBulleted, FormatListNumbered, FormatIndentDecrease, FormatIndentIncrease, InsertLink, Image as ImageIcon } from '@mui/icons-material'; import makeStyles from '@mui/styles/makeStyles'; import { useAfterInitialEffect, useConsolidatedRef, useUID } from '@pega/react-sdk-components/lib/hooks'; const useStyles = makeStyles(theme => ({ fieldLabel: { position: 'relative', transform: 'translate(0, 0px) scale(1)', marginBottom: '5px', color: theme.palette.text.secondary }, editorWrapper: { border: `1px solid ${theme.palette.divider}`, borderRadius: 4, minHeight: 130, '& .ProseMirror': { padding: '8px 12px', minHeight: 100, outline: 'none', fontFamily: theme.typography.fontFamily, fontSize: theme.typography.fontSize, color: theme.palette.text.primary, '& p.is-editor-empty:first-child::before': { color: theme.palette.text.secondary, opacity: 0.7, content: 'attr(data-placeholder)', float: 'left', height: 0, pointerEvents: 'none' }, '& a': { color: theme.palette.primary.main }, '& h1, & h2, & h3, & h4, & h5, & h6': { color: theme.palette.text.primary }, '& blockquote': { color: theme.palette.text.secondary, borderLeft: `4px solid ${theme.palette.primary.light}`, paddingLeft: 8 }, '& ul, & ol': { paddingLeft: 24 } } }, toolbar: { display: 'flex', flexWrap: 'wrap', gap: 2, padding: '4px 8px', borderTop: `1px solid ${theme.palette.divider}` } })); interface RichTextEditorProps { id?: string; defaultValue: string; label: string; labelHidden: boolean; info: string; testId: string; placeholder: string; disabled: boolean; required: boolean; readOnly: boolean; error: boolean; onBlur: React.EventHandler; onChange: React.EventHandler; } const RichTextEditor = forwardRef(function RichTextEditor(props: RichTextEditorProps, ref) { const theme = useTheme(); const classes = useStyles(); const uid = useUID(); const { id = uid, defaultValue, label, labelHidden, info, testId, placeholder, disabled, required, readOnly, error, onBlur, onChange } = props; const editorRef: any = useConsolidatedRef(ref); const fileInputRef = useRef(null); const editor = useEditor({ extensions: [StarterKit, Link.configure({ openOnClick: false }), Image.configure({ allowBase64: true }), Placeholder.configure({ placeholder })], content: defaultValue || '', editable: !readOnly && !disabled, onUpdate: ({ editor: ed }) => { onChange?.(ed.getHTML()); }, onBlur: ({ event }) => { onBlur?.(event); } }); useAfterInitialEffect(() => { editor?.setEditable(!readOnly && !disabled); }, [readOnly, disabled]); useEffect(() => { if (editor) { editorRef.current = editor; } }, [editor]); const handleImageUpload = useCallback(() => { fileInputRef.current?.click(); }, []); const onFileChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !editor) return; const reader = new FileReader(); reader.addEventListener('load', () => { editor .chain() .focus() .setImage({ src: reader.result as string }) .run(); }); reader.readAsDataURL(file); e.target.value = ''; }, [editor] ); const handleLinkInsert = useCallback(() => { if (!editor) return; const { from, to } = editor.state.selection; const previousUrl = editor.getAttributes('link').href; const url = window.prompt('URL', previousUrl); if (url === null) return; // Restore selection lost by the prompt dialog editor.chain().focus().setTextSelection({ from, to }).run(); if (url === '') { editor.chain().focus().extendMarkRange('link').unsetLink().run(); } else if (from === to) { // No text selected — insert URL as linked text editor .chain() .focus() .insertContent({ type: 'text', text: url, marks: [{ type: 'link', attrs: { href: url } }] }) .run(); } else { editor.chain().focus().setLink({ href: url }).run(); } }, [editor]); if (readOnly) { const value = defaultValue || '--'; return ( {!labelHidden && ( {label} )}
{info && {info}} ); } return ( {!labelHidden && ( {label} )} {!disabled && (
editor?.chain().focus().toggleBold().run()} color={editor?.isActive('bold') ? 'primary' : 'default'} > editor?.chain().focus().toggleItalic().run()} color={editor?.isActive('italic') ? 'primary' : 'default'} > editor?.chain().focus().toggleStrike().run()} color={editor?.isActive('strike') ? 'primary' : 'default'} > editor?.chain().focus().toggleBulletList().run()} color={editor?.isActive('bulletList') ? 'primary' : 'default'} > editor?.chain().focus().toggleOrderedList().run()} color={editor?.isActive('orderedList') ? 'primary' : 'default'} > editor?.chain().focus().liftListItem('listItem').run()}> editor?.chain().focus().sinkListItem('listItem').run()}>
)}
{info && {info}}
); }); export default RichTextEditor;