feat: implement FieldRichTextEditor component using TipTap and Mantine integration

This commit is contained in:
Firman Ramdhani
2026-06-22 11:24:44 +07:00
parent eaec6ec38f
commit edd71243a4
10 changed files with 737 additions and 38 deletions
@@ -0,0 +1,121 @@
import React, { useEffect } from 'react';
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import Link from '@tiptap/extension-link';
import TextAlign from '@tiptap/extension-text-align';
import { RichTextEditor } from '@mantine/tiptap';
import { Input } from '@mantine/core';
import { useTranslatedError } from '../useTranslatedError';
export type FieldRichTextEditorProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = UseControllerProps<TFieldValues, TName> & {
label?: React.ReactNode;
description?: React.ReactNode;
withAsterisk?: boolean;
};
function FieldRichTextEditorComponent<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(props: FieldRichTextEditorProps<TFieldValues, TName>) {
const {
name,
control,
rules,
shouldUnregister,
defaultValue,
disabled,
label,
description,
withAsterisk,
} = props;
const {
field,
fieldState: { error },
} = useController({
name,
control,
rules,
shouldUnregister,
defaultValue,
disabled,
});
const translatedError = useTranslatedError(error?.message);
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
TextAlign.configure({ types: ['heading', 'paragraph'], alignments: ['left', 'center', 'right', 'justify'] }),
],
content: field.value || '',
onUpdate({ editor }) {
field.onChange(editor.getHTML());
},
onBlur() {
field.onBlur();
},
});
useEffect(() => {
if (editor && field.value !== editor.getHTML()) {
editor.commands.setContent(field.value || '');
}
}, [field.value, editor]);
return (
<Input.Wrapper
label={label}
description={description}
withAsterisk={withAsterisk}
error={translatedError}
>
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignRight />
<RichTextEditor.AlignJustify />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
</Input.Wrapper>
);
}
// Wrap with React.memo for identical performance characteristics as withRHF components
export const FieldRichTextEditor = React.memo(FieldRichTextEditorComponent) as typeof FieldRichTextEditorComponent;
+1
View File
@@ -28,6 +28,7 @@ export { FieldNumberInput } from './fields/number-input.field';
export { FieldJsonInput } from './fields/json-input.field';
export { FieldPinInput } from './fields/pin-input.field';
export { FieldAutocomplete } from './fields/autocomplete.field';
export { FieldRichTextEditor } from './fields/rich-text.field';
// ---------------------------------------------------------------------------
// Selection Fields
+4
View File
@@ -1,4 +1,8 @@
export * from '@mantine/core';
export {
List,
TypographyStylesProvider,
} from '@mantine/core';
export * from './Form';
export * from './system-pages/coming-soon';