Skip to content

Commit 55d48b6

Browse files
committed
feat: create extension callout
1 parent 8152ed0 commit 55d48b6

File tree

21 files changed

+706
-7
lines changed

21 files changed

+706
-7
lines changed

docs/.vitepress/locale.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function getLocaleConfig(lang: string) {
8787
{ text: 'Blockquote', link: '/extensions/Blockquote/index.md' },
8888
{ text: 'Bold', link: '/extensions/Bold/index.md' },
8989
{ text: 'BulletList', link: '/extensions/BulletList/index.md' },
90+
{ text: 'Callout', link: '/extensions/Callout/index.md' },
9091
{ text: 'Clear', link: '/extensions/Clear/index.md' },
9192
{ text: 'Code', link: '/extensions/Code/index.md' },
9293
{ text: 'CodeBlock', link: '/extensions/CodeBlock/index.md' },

docs/extensions/Callout/index.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
description: Callout
3+
4+
next:
5+
text: Callout
6+
link: /extensions/Callout/index.md
7+
---
8+
9+
# Callout
10+
11+
The Callout extension allows you to add callout boxes with different styles to your editor.
12+
13+
## Usage
14+
15+
16+
```tsx
17+
import { RichTextProvider } from 'reactjs-tiptap-editor'
18+
19+
// Base Kit
20+
import { Document } from '@tiptap/extension-document'
21+
import { Text } from '@tiptap/extension-text'
22+
import { Paragraph } from '@tiptap/extension-paragraph'
23+
import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24+
import { HardBreak } from '@tiptap/extension-hard-break'
25+
import { TextStyle } from '@tiptap/extension-text-style';
26+
import { ListItem } from '@tiptap/extension-list';
27+
28+
// Extension
29+
import { Callout, RichTextCallout } from 'reactjs-tiptap-editor/callout'; // [!code ++]
30+
// ... other extensions
31+
32+
33+
// Import CSS
34+
import 'reactjs-tiptap-editor/style.css';
35+
36+
const extensions = [
37+
// Base Extensions
38+
Document,
39+
Text,
40+
Dropcursor,
41+
Gapcursor,
42+
HardBreak,
43+
Paragraph,
44+
TrailingNode,
45+
ListItem,
46+
TextStyle,
47+
Placeholder.configure({
48+
placeholder: 'Press \'/\' for commands',
49+
})
50+
51+
...
52+
// Import Extensions Here
53+
Callout// [!code ++]
54+
];
55+
56+
const RichTextToolbar = () => {
57+
return (
58+
<div className="flex items-center gap-2 flex-wrap border-b border-solid">
59+
<RichTextCallout /> {/* [!code ++] */}
60+
</div>
61+
)
62+
}
63+
64+
const App = () => {
65+
const editor = useEditor({
66+
textDirection: 'auto', // global text direction
67+
extensions,
68+
});
69+
70+
return (
71+
<RichTextProvider
72+
editor={editor}
73+
>
74+
<RichTextToolbar />
75+
76+
<EditorContent
77+
editor={editor}
78+
/>
79+
</RichTextProvider>
80+
);
81+
};
82+
```
83+
84+
85+
## Options
86+
87+
### shortcutKeys
88+
89+
Type: `string[]`\
90+
Default: `['shift', 'mod', '8']`
91+
92+
Keyboard shortcuts for the extension.

docs/guide/bubble-menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,4 @@ The system provides the following default bubble menus:
148148
| RichTextBubbleMermaid | Provides mermaid-related operations like size, link , etc. | mermaid |
149149
| RichTextBubbleTwitter | Provides twitter-related operations like size, link , etc. | twitter |
150150
| RichTextBubbleMenuDragHandle | Provides a drag handle to move the bubble menu around the editor area. | N/A |
151+
| RichTextBubbleCallout | Provides callout-related operations like style, content, etc. | callout |

playground/src/App.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@ import { Drawer, RichTextDrawer } from 'reactjs-tiptap-editor/drawer';
6060
import { Twitter, RichTextTwitter } from 'reactjs-tiptap-editor/twitter';
6161
import { Mention } from 'reactjs-tiptap-editor/mention';
6262
import { CodeView, RichTextCodeView } from 'reactjs-tiptap-editor/codeview';
63+
import { Callout, RichTextCallout} from 'reactjs-tiptap-editor/callout';
6364

6465
// Slash Command
6566
import { SlashCommand, SlashCommandList } from 'reactjs-tiptap-editor/slashcommand';
6667

6768

6869
// Bubble
6970
import {
71+
RichTextBubbleCallout,
7072
RichTextBubbleColumns,
7173
RichTextBubbleDrawer,
7274
RichTextBubbleExcalidraw,
@@ -320,7 +322,7 @@ const extensions = [
320322
}),
321323
SlashCommand,
322324
CodeView,
323-
325+
Callout
324326
// Collaboration.configure({
325327
// document: hocuspocusProvider.document,
326328
// }),
@@ -332,7 +334,7 @@ const extensions = [
332334
// }),
333335
]
334336

335-
const DEFAULT = ``
337+
const DEFAULT = `<div class="callout" dir="auto" type="note" title="1" body="1"></div><div class="callout" dir="auto" type="tip" title="2" body="2"></div><div class="callout" dir="auto" type="important" title="3" body="3"></div><div class="callout" dir="auto" type="warning" title="4" body="4"></div><div class="callout" dir="auto" type="caution" title="5" body="5"></div><p dir="auto"></p>`
336338

337339
function debounce(func: any, wait: number) {
338340
let timeout: NodeJS.Timeout
@@ -494,6 +496,7 @@ const RichTextToolbar = () => {
494496
<RichTextDrawer />
495497
<RichTextTwitter />
496498
<RichTextCodeView />
499+
<RichTextCallout />
497500
</div>
498501
}
499502

@@ -547,6 +550,7 @@ function App() {
547550
/>
548551

549552
{/* Bubble */}
553+
<RichTextBubbleCallout />
550554
<RichTextBubbleColumns />
551555
<RichTextBubbleDrawer />
552556
<RichTextBubbleExcalidraw />
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
import { BubbleMenu } from '@tiptap/react/menus';
4+
5+
import { ActionButton } from '@/components/ActionButton';
6+
import { Button, Input, Label } from '@/components/ui';
7+
import { Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
8+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
9+
import { Callout } from '@/extensions/Callout';
10+
import { useAttributes } from '@/hooks/useAttributes';
11+
import { useLocale } from '@/locales';
12+
import { useEditorInstance } from '@/store/editor';
13+
import { useEditableEditor } from '@/store/store';
14+
import { deleteNode } from '@/utils/delete-node';
15+
16+
const CALLOUT_TYPES = [
17+
{ value: 'note', label: 'Note', icon: 'Info' },
18+
{ value: 'tip', label: 'Tip', icon: 'Lightbulb' },
19+
{ value: 'important', label: 'Important', icon: 'AlertCircle' },
20+
{ value: 'warning', label: 'Warning', icon: 'TriangleAlert' },
21+
{ value: 'caution', label: 'Caution', icon: 'OctagonAlert' },
22+
] as const;
23+
24+
interface ICalloutAttrs {
25+
type?: string
26+
title?: string
27+
body?: string
28+
}
29+
30+
export function RichTextBubbleCallout() {
31+
const editable = useEditableEditor();
32+
const editor = useEditorInstance();
33+
const { t } = useLocale();
34+
35+
const { type, title, body } = useAttributes<ICalloutAttrs>(editor, Callout.name, {
36+
type: 'note',
37+
title: '',
38+
body: ''
39+
});
40+
41+
const [visible, setVisible] = useState(false);
42+
const [calloutType, setCalloutType] = useState('note');
43+
const [calloutTitle, setCalloutTitle] = useState('');
44+
const [calloutBody, setCalloutBody] = useState('');
45+
46+
useEffect(() => {
47+
if (visible) {
48+
setCalloutType(type || 'note');
49+
setCalloutTitle(title || '');
50+
setCalloutBody(body || '');
51+
}
52+
}, [visible, type, title, body]);
53+
54+
const handleUpdate = useCallback(() => {
55+
if (!editor) return;
56+
57+
// Update attributes
58+
editor
59+
.chain()
60+
.updateAttributes(Callout.name, {
61+
type: calloutType,
62+
title: calloutTitle,
63+
body: calloutBody,
64+
})
65+
.focus()
66+
.run();
67+
68+
setVisible(false);
69+
}, [editor, calloutType, calloutTitle, calloutBody]);
70+
71+
const handleCancel = useCallback(() => {
72+
setVisible(false);
73+
}, []);
74+
75+
const handleDelete = useCallback(() => {
76+
deleteNode(Callout.name, editor);
77+
}, [editor]);
78+
79+
const shouldShow = useCallback(() => {
80+
return editor.isActive(Callout.name);
81+
}, [editor]);
82+
83+
if (!editable) {
84+
return <></>;
85+
}
86+
87+
return (
88+
<BubbleMenu
89+
editor={editor}
90+
options={{ placement: 'top', offset: 8, flip: true }}
91+
pluginKey="RichTextBubbleCallout"
92+
shouldShow={shouldShow}
93+
>
94+
<div className="richtext-flex richtext-items-center richtext-gap-2 richtext-rounded-md !richtext-border !richtext-border-solid !richtext-border-border richtext-bg-popover richtext-p-1 richtext-text-popover-foreground richtext-shadow-md richtext-outline-none">
95+
<Dialog onOpenChange={setVisible}
96+
open={visible}
97+
>
98+
<DialogTrigger asChild>
99+
<ActionButton icon="Pencil"
100+
tooltip={t('editor.callout.edit.title')}
101+
/>
102+
</DialogTrigger>
103+
104+
<DialogContent>
105+
<DialogTitle>
106+
{t('editor.callout.edit.title')}
107+
</DialogTitle>
108+
109+
<div className="richtext-space-y-4 richtext-py-4">
110+
<div className="richtext-space-y-2">
111+
<Label>
112+
{t('editor.callout.dialog.type')}
113+
</Label>
114+
115+
<Select onValueChange={setCalloutType}
116+
value={calloutType}
117+
>
118+
<SelectTrigger>
119+
<SelectValue className='richtext-text-accent'
120+
placeholder={t('editor.callout.dialog.type.placeholder')}
121+
/>
122+
</SelectTrigger>
123+
124+
<SelectContent>
125+
{CALLOUT_TYPES.map((type) => (
126+
<SelectItem key={type.value}
127+
value={type.value}
128+
>
129+
{t(`editor.callout.type.${type.value}`)}
130+
</SelectItem>
131+
))}
132+
</SelectContent>
133+
</Select>
134+
</div>
135+
136+
<div className="richtext-space-y-2">
137+
<Label>
138+
{t('editor.callout.dialog.title.label')}
139+
</Label>
140+
141+
<Input
142+
onChange={(e) => setCalloutTitle(e.target.value)}
143+
placeholder={t('editor.callout.dialog.title.placeholder')}
144+
type="text"
145+
value={calloutTitle}
146+
/>
147+
</div>
148+
149+
<div className="richtext-space-y-2">
150+
<Label>
151+
{t('editor.callout.dialog.body.label')}
152+
</Label>
153+
154+
<Input
155+
onChange={(e) => setCalloutBody(e.target.value)}
156+
placeholder={t('editor.callout.dialog.body.placeholder')}
157+
type="text"
158+
value={calloutBody}
159+
/>
160+
</div>
161+
</div>
162+
163+
<DialogFooter>
164+
<Button onClick={handleCancel}
165+
variant="outline"
166+
>
167+
{t('editor.callout.dialog.button.cancel')}
168+
</Button>
169+
170+
<Button onClick={handleUpdate}>
171+
{t('editor.callout.dialog.button.apply')}
172+
</Button>
173+
</DialogFooter>
174+
</DialogContent>
175+
</Dialog>
176+
177+
<ActionButton action={handleDelete}
178+
icon="Trash2"
179+
tooltip={t('editor.delete')}
180+
/>
181+
</div>
182+
</BubbleMenu>
183+
);
184+
}

src/components/Bubble/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './RichTextBubbleCallout';
12
export * from './RichTextBubbleColumns';
23
export * from './RichTextBubbleDrawer';
34
export * from './RichTextBubbleExcalidraw';

src/components/icons/icons.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ import {
8080
WrapText,
8181
Loader2 as Loader,
8282
X,
83-
ExternalLink
83+
ExternalLink,
84+
NotebookPen
8485
} from 'lucide-react';
8586

8687
import {
@@ -222,5 +223,6 @@ export const icons = {
222223
Loader,
223224
X,
224225
Html,
225-
ExternalLink
226+
ExternalLink,
227+
Callout: NotebookPen
226228
} as any;

src/components/ui/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const buttonVariants = cva(
1414
destructive:
1515
'richtext-bg-destructive richtext-text-destructive-foreground hover:richtext-bg-destructive/90',
1616
outline:
17-
'richtext-border richtext-border-input richtext-bg-background hover:richtext-bg-accent hover:richtext-text-accent-foreground',
17+
'richtext-border richtext-border-input richtext-bg-background richtext-text-foreground hover:richtext-bg-accent hover:richtext-text-accent-foreground',
1818
secondary:
1919
'richtext-bg-secondary richtext-text-secondary-foreground hover:richtext-bg-secondary/80',
2020
ghost: 'hover:richtext-bg-accent hover:richtext-text-accent-foreground',

src/components/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './popover';
66
export * from './separator';
77
export * from './switch';
88
export * from './tabs';
9+
export * from './textarea';
910
export * from './toast';
1011
export * from './toggle';
1112
export * from './tooltip';

src/components/ui/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
1212
ref={ref}
1313
type={type}
1414
className={cn(
15-
'richtext-flex richtext-h-10 richtext-w-full richtext-rounded-md !richtext-border richtext-border-input richtext-bg-transparent richtext-px-3 richtext-py-2 richtext-text-sm richtext-ring-offset-background file:richtext-border-0 file:richtext-bg-transparent file:richtext-text-sm file:richtext-font-medium placeholder:richtext-text-muted-foreground focus-visible:richtext-outline-none focus-visible:richtext-ring-1 focus-visible:richtext-ring-ring focus-visible:richtext-ring-offset-1 disabled:richtext-cursor-not-allowed disabled:richtext-opacity-50 ',
15+
'richtext-flex richtext-h-10 richtext-w-full richtext-rounded-md !richtext-border richtext-border-input richtext-bg-transparent richtext-px-3 richtext-py-2 richtext-text-sm richtext-ring-offset-background file:richtext-border-0 file:richtext-bg-transparent file:richtext-text-sm file:richtext-font-medium placeholder:richtext-text-muted-foreground focus-visible:richtext-outline-none focus-visible:richtext-ring-1 focus-visible:richtext-ring-ring focus-visible:richtext-ring-offset-1 disabled:richtext-cursor-not-allowed disabled:richtext-opacity-50 richtext-text-foreground',
1616
className,
1717
)}
1818
{...props}

0 commit comments

Comments
 (0)