Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ yarn-error.log*

# worktrees
.worktrees

# serena
.serena/
10 changes: 10 additions & 0 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
"dist/**"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./core": {
"import": {
"types": "./dist/core/index.d.mts",
Expand Down
136 changes: 136 additions & 0 deletions packages/editor/src/email-editor/email-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { Content, Editor, Extensions, JSONContent } from '@tiptap/core';
import { UndoRedo } from '@tiptap/extensions';
import {
EditorProvider,
type UseEditorOptions,
useCurrentEditor,
} from '@tiptap/react';
import { forwardRef, type Ref, useImperativeHandle, useMemo } from 'react';
import { createDropHandler } from '../core/create-drop-handler';
import {
createPasteHandler,
type UploadImageHandler,
} from '../core/create-paste-handler';
import { composeReactEmail } from '../core/serializer/compose-react-email';
import { StarterKit } from '../extensions';
import { EmailTheming } from '../plugins/email-theming/extension';
import { BubbleMenuDefault } from '../ui/bubble-menu/default';
import { ButtonBubbleMenuDefault } from '../ui/button-bubble-menu/default';
import { ImageBubbleMenuDefault } from '../ui/image-bubble-menu/default';
import { LinkBubbleMenuDefault } from '../ui/link-bubble-menu/default';
import { SlashCommandRoot } from '../ui/slash-command/root';
import '../ui/themes/default.css';

export interface EmailEditorRef {
export: () => Promise<{ html: string; text: string }>;
getJSON: () => JSONContent;
getHTML: () => string;
editor: Editor | null;
}

export interface EmailEditorProps {
content?: Content;
onChange?: (editor: Editor) => void;
onUploadImage?: UploadImageHandler;
onReady?: (editor: Editor) => void;
theme?: 'basic' | 'minimal';
editable?: boolean;
placeholder?: string;
bubbleMenu?: {
excludeNodes?: string[];
excludeMarks?: string[];
};
extensions?: Extensions;
className?: string;
}

function RefBridge({ editorRef }: { editorRef: Ref<EmailEditorRef> }) {
const { editor } = useCurrentEditor();

useImperativeHandle(
editorRef,
() => ({
export: async () => {
if (!editor) {
return { html: '', text: '' };
}
return composeReactEmail({ editor });
},
getJSON: () => editor?.getJSON() ?? { type: 'doc', content: [] },
getHTML: () => editor?.getHTML() ?? '',
editor,
}),
[editor],
);

return null;
}

export const EmailEditor = forwardRef<EmailEditorRef, EmailEditorProps>(
function EmailEditor(
Comment thread
joaopcm marked this conversation as resolved.
Outdated
{
content,
onChange,
onUploadImage,
onReady,
theme = 'basic',
editable = true,
placeholder,
bubbleMenu,
extensions: extensionsProp,
className,
},
ref,
) {
const extensions = useMemo(() => {
if (extensionsProp) {
return extensionsProp;
}

return [
StarterKit.configure({
Placeholder: placeholder ? { placeholder } : undefined,
}),
UndoRedo,
Comment thread
gabrielmfern marked this conversation as resolved.
EmailTheming.configure({ theme }),
];
}, [extensionsProp, theme, placeholder]);

const editorProps: UseEditorOptions['editorProps'] = useMemo(
() => ({
handlePaste: createPasteHandler({
onUploadImage,
extensions,
}),
handleDrop: createDropHandler({
onUploadImage,
}),
}),
[onUploadImage, extensions],
);

return (
<EditorProvider
Comment thread
joaopcm marked this conversation as resolved.
key={theme}
extensions={extensions}
content={content}
editable={editable}
immediatelyRender={false}
editorProps={editorProps}
editorContainerProps={{ className }}
onCreate={({ editor }) => onReady?.(editor)}
onUpdate={({ editor }) => onChange?.(editor)}
>
<RefBridge editorRef={ref} />
<BubbleMenuDefault
excludeNodes={bubbleMenu?.excludeNodes ?? ['button']}
excludeMarks={bubbleMenu?.excludeMarks ?? ['link']}
/>
<LinkBubbleMenuDefault />
<ButtonBubbleMenuDefault />
<ImageBubbleMenuDefault />
<SlashCommandRoot />
</EditorProvider>
);
},
);
5 changes: 5 additions & 0 deletions packages/editor/src/email-editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
EmailEditor,
type EmailEditorProps,
type EmailEditorRef,
} from './email-editor';
5 changes: 5 additions & 0 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
EmailEditor,
type EmailEditorProps,
type EmailEditorRef,
} from './email-editor';
1 change: 1 addition & 0 deletions packages/editor/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineConfig } from 'tsdown';

export default defineConfig({
entry: {
index: 'src/index.ts',
'core/index': 'src/core/index.ts',
'extensions/index': 'src/extensions/index.ts',
'plugins/index': 'src/plugins/index.ts',
Expand Down
Loading