@groupher/rich-editor 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/rich-editor.es.js +31268 -20037
  2. package/dist/rich-editor.umd.js +77 -96
  3. package/package.json +17 -2
  4. package/src/RichEditor.tsx +204 -92
  5. package/src/components/editor/editor-kit.tsx +27 -0
  6. package/src/components/editor/plugins/autoformat-kit.tsx +99 -0
  7. package/src/components/editor/plugins/callout-kit.tsx +7 -0
  8. package/src/components/editor/plugins/emoji-kit.tsx +14 -0
  9. package/src/components/editor/plugins/indent-kit.tsx +12 -0
  10. package/src/components/editor/plugins/link-kit.tsx +13 -0
  11. package/src/components/editor/plugins/list-kit.tsx +17 -0
  12. package/src/components/editor/plugins/mention-kit.tsx +17 -0
  13. package/src/components/editor/plugins/slash-kit.tsx +15 -0
  14. package/src/components/editor/plugins/toggle-kit.tsx +7 -0
  15. package/src/components/ui/action-bar.tsx +208 -0
  16. package/src/components/ui/block-list.tsx +94 -0
  17. package/src/components/ui/button.tsx +49 -50
  18. package/src/components/ui/callout-node.tsx +65 -0
  19. package/src/components/ui/editor-static.tsx +44 -44
  20. package/src/components/ui/editor.tsx +107 -107
  21. package/src/components/ui/emoji-node.tsx +71 -0
  22. package/src/components/ui/emoji-toolbar-button.tsx +618 -0
  23. package/src/components/ui/floating-toolbar.tsx +86 -0
  24. package/src/components/ui/inline-combobox.tsx +414 -0
  25. package/src/components/ui/link-node.tsx +31 -0
  26. package/src/components/ui/link-toolbar-button.tsx +33 -0
  27. package/src/components/ui/mention-node.tsx +126 -0
  28. package/src/components/ui/slash-node.tsx +191 -0
  29. package/src/components/ui/toggle-node.tsx +36 -0
  30. package/src/components/ui/toolbar.tsx +10 -10
  31. package/src/hooks/use-debounce.ts +15 -0
  32. package/src/hooks/use-mounted.ts +11 -0
  33. package/src/i18n.tsx +155 -0
  34. package/src/main.tsx +35 -14
  35. package/src/mention-context.tsx +32 -0
  36. package/src/vite-env.d.ts +7 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@groupher/rich-editor",
3
3
  "private": false,
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "main": "dist/rich-editor.umd.js",
6
6
  "module": "dist/rich-editor.es.js",
7
7
  "type": "module",
@@ -20,14 +20,29 @@
20
20
  "slate-react": "0.117.4"
21
21
  },
22
22
  "dependencies": {
23
- "@groupher/rich-editor": "^0.0.7",
23
+ "@groupher/rich-editor": "^0.0.8",
24
+ "@ariakit/react": "^0.4.12",
25
+ "@emoji-mart/data": "^1.2.1",
24
26
  "@platejs/basic-nodes": "^49.0.0",
27
+ "@platejs/autoformat": "^49.0.0",
28
+ "@platejs/callout": "^49.0.0",
29
+ "@platejs/combobox": "^49.0.0",
30
+ "@platejs/emoji": "^49.0.0",
31
+ "@platejs/floating": "^49.0.0",
32
+ "@platejs/indent": "^49.0.0",
33
+ "@platejs/link": "^49.0.0",
34
+ "@platejs/list": "^49.0.0",
35
+ "@platejs/mention": "^49.0.0",
36
+ "@platejs/slash-command": "^49.0.0",
37
+ "@platejs/toggle": "^49.0.0",
25
38
  "@radix-ui/react-dropdown-menu": "^2.1.16",
26
39
  "@radix-ui/react-separator": "^1.1.7",
27
40
  "@radix-ui/react-slot": "^1.2.3",
28
41
  "@radix-ui/react-toolbar": "^1.1.11",
29
42
  "@radix-ui/react-tooltip": "^1.2.8",
43
+ "@radix-ui/react-popover": "^1.1.7",
30
44
  "@tailwindcss/vite": "^4.1.12",
45
+ "@udecode/cn": "^44.0.1",
31
46
  "class-variance-authority": "^0.7.1",
32
47
  "clsx": "^2.1.1",
33
48
  "lucide-react": "^0.542.0",
@@ -1,102 +1,214 @@
1
+ import * as React from "react";
2
+
1
3
  import type { Value } from "platejs";
2
4
 
3
- import {
4
- BlockquotePlugin,
5
- H1Plugin,
6
- H2Plugin,
7
- H3Plugin,
8
- BoldPlugin,
9
- ItalicPlugin,
10
- UnderlinePlugin,
11
- } from "@platejs/basic-nodes/react";
5
+ import { Bold, Italic, Strikethrough, Underline } from "lucide-react";
12
6
  import { Plate, usePlateEditor } from "platejs/react";
13
7
 
8
+ import { EditorKit } from "@/components/editor/editor-kit";
9
+ import { ActionBar } from "@/components/ui/action-bar";
10
+ import { Button } from "@/components/ui/button";
14
11
  import { Editor, EditorContainer } from "@/components/ui/editor";
15
- import { BlockquoteElement } from "@/components/ui/blockquote-node";
16
- import { H1Element, H2Element, H3Element } from "@/components/ui/heading-node";
17
- import { ToolbarButton } from "@/components/ui/toolbar"; // Generic toolbar button
18
-
19
- import { FixedToolbar } from "@/components/ui/fixed-toolbar";
12
+ import { FloatingToolbar } from "@/components/ui/floating-toolbar";
13
+ import { LinkToolbarButton } from "@/components/ui/link-toolbar-button";
20
14
  import { MarkToolbarButton } from "@/components/ui/mark-toolbar-button";
15
+ import { I18nProvider, type TLocale, useI18n } from "@/i18n";
16
+ import { MentionProvider, type TMentionOption } from "@/mention-context";
17
+
18
+ const storageKey = "groupher-rich-editor-value";
21
19
 
22
- import { EditorStatic } from "@/components/ui/editor-static";
23
-
24
- const initialValue: Value = [
25
- {
26
- children: [{ text: "Title" }],
27
- type: "h3",
28
- },
29
- {
30
- children: [{ text: "This is a quote." }],
31
- type: "blockquote",
32
- },
33
- {
34
- type: "p",
35
- children: [
36
- { text: "Hello! Try out the " },
37
- { text: "bold", bold: true },
38
- { text: ", " },
39
- { text: "italic", italic: true },
40
- { text: ", and " },
41
- { text: "underline", underline: true },
42
- { text: " formatting." },
43
- ],
44
- },
20
+ const defaultValue: Value = [
21
+ {
22
+ type: "h1",
23
+ children: [{ text: "Plate Editor" }],
24
+ },
25
+ {
26
+ type: "p",
27
+ children: [
28
+ { text: "Use " },
29
+ { text: "/", bold: true },
30
+ { text: " to open the slash menu and " },
31
+ { text: "@", bold: true },
32
+ { text: " to mention." },
33
+ ],
34
+ },
35
+ {
36
+ type: "blockquote",
37
+ children: [{ text: "A short quote block for emphasis." }],
38
+ },
39
+ {
40
+ type: "callout",
41
+ icon: "💡",
42
+ children: [{ text: "Callout blocks highlight important notes." }],
43
+ },
44
+ {
45
+ type: "toggle",
46
+ id: "toggle-1",
47
+ children: [{ text: "Toggle blocks can hide content." }],
48
+ },
49
+ {
50
+ type: "p",
51
+ indent: 1,
52
+ listStyleType: "disc",
53
+ children: [{ text: "Bulleted list item." }],
54
+ },
55
+ {
56
+ type: "p",
57
+ indent: 1,
58
+ listStyleType: "decimal",
59
+ children: [{ text: "Numbered list item." }],
60
+ },
61
+ {
62
+ type: "p",
63
+ indent: 1,
64
+ listStyleType: "todo",
65
+ checked: false,
66
+ children: [{ text: "Todo list item." }],
67
+ },
45
68
  ];
46
69
 
47
- export default function RichEditor() {
48
- const editor = usePlateEditor({
49
- plugins: [
50
- BoldPlugin,
51
- ItalicPlugin,
52
- UnderlinePlugin,
53
- H1Plugin.withComponent(H1Element),
54
- H2Plugin.withComponent(H2Element),
55
- H3Plugin.withComponent(H3Element),
56
- BlockquotePlugin.withComponent(BlockquoteElement),
57
- ], // mark plugins
58
- // value: initialValue, // initial content
59
- value: () => {
60
- const savedValue = localStorage.getItem("installation-react-demo");
61
- return savedValue ? JSON.parse(savedValue) : initialValue;
62
- },
63
- });
64
-
65
- return (
66
- <div className="m-5 debug">
67
- <Plate
68
- editor={editor}
69
- onChange={({ value }) => {
70
- console.log("## on change: ", value)
71
- localStorage.setItem("installation-react-demo", JSON.stringify(value));
72
- }}
73
- >
74
- <FixedToolbar className="justify-start rounded-t-lg">
75
- <ToolbarButton onClick={() => editor.tf.h1.toggle()}>H1</ToolbarButton>
76
- <ToolbarButton onClick={() => editor.tf.h2.toggle()}>H2</ToolbarButton>
77
- <ToolbarButton onClick={() => editor.tf.h3.toggle()}>H3</ToolbarButton>
78
- <ToolbarButton onClick={() => editor.tf.blockquote.toggle()}>Quote</ToolbarButton>
79
-
80
- <MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
81
- B
82
- </MarkToolbarButton>
83
- <MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">
84
- I
85
- </MarkToolbarButton>
86
- <MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">
87
- U
88
- </MarkToolbarButton>
89
-
90
- <div className="flex-1" />
91
- <ToolbarButton className="px-2" onClick={() => editor.tf.setValue(initialValue)}>
92
- Reset
93
- </ToolbarButton>
94
- </FixedToolbar>
95
- <EditorContainer>
96
- <Editor placeholder="Type your amazing content here..." />
97
- <EditorStatic editor={editor} />
98
- </EditorContainer>
99
- </Plate>
100
- </div>
101
- );
70
+ type TRichEditorProps = {
71
+ locale?: TLocale;
72
+ mentionOptions?: TMentionOption[];
73
+ onMentionSearch?: (query: string) => void;
74
+ };
75
+
76
+ function RichEditorInner() {
77
+ const i18n = useI18n();
78
+ const [value, setValue] = React.useState<Value>(() => {
79
+ if (typeof window === "undefined") return defaultValue;
80
+
81
+ const savedValue = localStorage.getItem(storageKey);
82
+
83
+ if (!savedValue) return defaultValue;
84
+
85
+ try {
86
+ return JSON.parse(savedValue) as Value;
87
+ } catch {
88
+ return defaultValue;
89
+ }
90
+ });
91
+ const [jsonInput, setJsonInput] = React.useState("");
92
+ const [jsonError, setJsonError] = React.useState("");
93
+ const [readOnlyValue, setReadOnlyValue] = React.useState<Value>(value);
94
+
95
+ const editor = usePlateEditor({
96
+ plugins: EditorKit,
97
+ value,
98
+ });
99
+ const readOnlyEditor = usePlateEditor({
100
+ plugins: EditorKit,
101
+ value: readOnlyValue,
102
+ });
103
+
104
+ const handleExport = React.useCallback(() => {
105
+ const nextJson = JSON.stringify(value, null, 2);
106
+ setJsonInput(nextJson);
107
+ setReadOnlyValue(value);
108
+ setJsonError("");
109
+ }, [value]);
110
+
111
+ const handleRenderReadonly = React.useCallback(() => {
112
+ try {
113
+ const parsed = JSON.parse(jsonInput) as Value;
114
+ setReadOnlyValue(parsed);
115
+ setJsonError("");
116
+ } catch {
117
+ setJsonError(i18n.export.invalidJson);
118
+ }
119
+ }, [i18n.export.invalidJson, jsonInput]);
120
+
121
+ React.useEffect(() => {
122
+ readOnlyEditor.tf.setValue(readOnlyValue);
123
+ }, [readOnlyEditor, readOnlyValue]);
124
+
125
+ return (
126
+ <div className="m-6 space-y-6">
127
+ <Plate
128
+ editor={editor}
129
+ onChange={({ value }) => {
130
+ setValue(value);
131
+ localStorage.setItem(storageKey, JSON.stringify(value));
132
+ }}
133
+ >
134
+ <FloatingToolbar>
135
+ <MarkToolbarButton nodeType="bold" tooltip={i18n.toolbar.bold}>
136
+ <Bold className="size-4" />
137
+ </MarkToolbarButton>
138
+ <MarkToolbarButton nodeType="italic" tooltip={i18n.toolbar.italic}>
139
+ <Italic className="size-4" />
140
+ </MarkToolbarButton>
141
+ <MarkToolbarButton
142
+ nodeType="underline"
143
+ tooltip={i18n.toolbar.underline}
144
+ >
145
+ <Underline className="size-4" />
146
+ </MarkToolbarButton>
147
+ <MarkToolbarButton
148
+ nodeType="strikethrough"
149
+ tooltip={i18n.toolbar.strikethrough}
150
+ >
151
+ <Strikethrough className="size-4" />
152
+ </MarkToolbarButton>
153
+ <LinkToolbarButton />
154
+ </FloatingToolbar>
155
+
156
+ <EditorContainer>
157
+ <Editor placeholder={i18n.placeholder} />
158
+ <ActionBar />
159
+ </EditorContainer>
160
+ </Plate>
161
+
162
+ <div className="rounded-lg border border-border bg-card p-4">
163
+ <div className="flex flex-wrap items-center justify-between gap-3">
164
+ <h3 className="text-sm font-semibold">{i18n.export.title}</h3>
165
+ <div className="flex items-center gap-2">
166
+ <Button size="sm" onClick={handleExport}>
167
+ {i18n.export.button}
168
+ </Button>
169
+ <Button size="sm" variant="outline" onClick={handleRenderReadonly}>
170
+ {i18n.export.loadButton}
171
+ </Button>
172
+ </div>
173
+ </div>
174
+ <textarea
175
+ className="mt-3 h-40 w-full rounded-md border border-input bg-background p-3 text-xs font-mono text-foreground"
176
+ placeholder={i18n.export.placeholder}
177
+ value={jsonInput}
178
+ onChange={(event) => setJsonInput(event.target.value)}
179
+ />
180
+ {jsonError ? (
181
+ <p className="mt-2 text-xs text-destructive">{jsonError}</p>
182
+ ) : null}
183
+ </div>
184
+
185
+ <div className="rounded-lg border border-border bg-card">
186
+ <div className="border-b border-border px-4 py-2 text-sm font-semibold">
187
+ {i18n.export.readonlyTitle}
188
+ </div>
189
+ <Plate editor={readOnlyEditor} readOnly>
190
+ <EditorContainer>
191
+ <Editor variant="demo" />
192
+ </EditorContainer>
193
+ </Plate>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export default function RichEditor({
200
+ locale,
201
+ mentionOptions,
202
+ onMentionSearch,
203
+ }: TRichEditorProps) {
204
+ return (
205
+ <I18nProvider locale={locale}>
206
+ <MentionProvider
207
+ mentionOptions={mentionOptions}
208
+ onMentionSearch={onMentionSearch}
209
+ >
210
+ <RichEditorInner />
211
+ </MentionProvider>
212
+ </I18nProvider>
213
+ );
102
214
  }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
4
+ import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit';
5
+ import { AutoformatKit } from '@/components/editor/plugins/autoformat-kit';
6
+ import { CalloutKit } from '@/components/editor/plugins/callout-kit';
7
+ import { EmojiKit } from '@/components/editor/plugins/emoji-kit';
8
+ import { IndentKit } from '@/components/editor/plugins/indent-kit';
9
+ import { LinkKit } from '@/components/editor/plugins/link-kit';
10
+ import { ListKit } from '@/components/editor/plugins/list-kit';
11
+ import { MentionKit } from '@/components/editor/plugins/mention-kit';
12
+ import { SlashKit } from '@/components/editor/plugins/slash-kit';
13
+ import { ToggleKit } from '@/components/editor/plugins/toggle-kit';
14
+
15
+ export const EditorKit = [
16
+ ...BasicBlocksKit,
17
+ ...BasicMarksKit,
18
+ ...AutoformatKit,
19
+ ...EmojiKit,
20
+ ...IndentKit,
21
+ ...ListKit,
22
+ ...ToggleKit,
23
+ ...CalloutKit,
24
+ ...LinkKit,
25
+ ...MentionKit,
26
+ ...SlashKit,
27
+ ];
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import type { AutoformatRule } from '@platejs/autoformat';
4
+
5
+ import { AutoformatPlugin } from '@platejs/autoformat';
6
+ import { toggleList } from '@platejs/list';
7
+ import { KEYS } from 'platejs';
8
+ import type { PlateEditor } from 'platejs/react';
9
+
10
+ const autoformatBlocks: AutoformatRule[] = [
11
+ {
12
+ match: '# ',
13
+ mode: 'block',
14
+ type: KEYS.h1,
15
+ },
16
+ {
17
+ match: '## ',
18
+ mode: 'block',
19
+ type: KEYS.h2,
20
+ },
21
+ {
22
+ match: '### ',
23
+ mode: 'block',
24
+ type: KEYS.h3,
25
+ },
26
+ {
27
+ match: '> ',
28
+ mode: 'block',
29
+ type: KEYS.blockquote,
30
+ },
31
+ ];
32
+
33
+ const autoformatLists: AutoformatRule[] = [
34
+ {
35
+ match: ['* ', '- '],
36
+ mode: 'block',
37
+ type: 'list',
38
+ format: (editor: PlateEditor) => {
39
+ toggleList(editor, {
40
+ listStyleType: KEYS.ul,
41
+ });
42
+ },
43
+ },
44
+ {
45
+ match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
46
+ matchByRegex: true,
47
+ mode: 'block',
48
+ type: 'list',
49
+ format: (editor: PlateEditor, { matchString }: { matchString: string }) => {
50
+ toggleList(editor, {
51
+ listRestartPolite: Number(matchString) || 1,
52
+ listStyleType: KEYS.ol,
53
+ });
54
+ },
55
+ },
56
+ {
57
+ match: ['[] '],
58
+ mode: 'block',
59
+ type: 'list',
60
+ format: (editor: PlateEditor) => {
61
+ toggleList(editor, {
62
+ listStyleType: KEYS.listTodo,
63
+ });
64
+ editor.tf.setNodes({
65
+ checked: false,
66
+ listStyleType: KEYS.listTodo,
67
+ });
68
+ },
69
+ },
70
+ {
71
+ match: ['[x] '],
72
+ mode: 'block',
73
+ type: 'list',
74
+ format: (editor: PlateEditor) => {
75
+ toggleList(editor, {
76
+ listStyleType: KEYS.listTodo,
77
+ });
78
+ editor.tf.setNodes({
79
+ checked: true,
80
+ listStyleType: KEYS.listTodo,
81
+ });
82
+ },
83
+ },
84
+ ];
85
+
86
+ export const AutoformatKit = [
87
+ AutoformatPlugin.configure({
88
+ options: {
89
+ enableUndoOnDelete: true,
90
+ rules: [...autoformatBlocks, ...autoformatLists].map((rule) => ({
91
+ ...rule,
92
+ query: (editor: PlateEditor) =>
93
+ !editor.api.some({
94
+ match: { type: editor.getType(KEYS.codeBlock) },
95
+ }),
96
+ })),
97
+ },
98
+ }),
99
+ ];
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ import { CalloutPlugin } from '@platejs/callout/react';
4
+
5
+ import { CalloutElement } from '@/components/ui/callout-node';
6
+
7
+ export const CalloutKit = [CalloutPlugin.withComponent(CalloutElement)];
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import emojiMartData from '@emoji-mart/data';
4
+ import type { EmojiMartData } from '@emoji-mart/data';
5
+ import { EmojiInputPlugin, EmojiPlugin } from '@platejs/emoji/react';
6
+
7
+ import { EmojiInputElement } from '@/components/ui/emoji-node';
8
+
9
+ export const EmojiKit = [
10
+ EmojiPlugin.configure({
11
+ options: { data: emojiMartData as EmojiMartData },
12
+ }),
13
+ EmojiInputPlugin.withComponent(EmojiInputElement),
14
+ ];
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { IndentPlugin } from '@platejs/indent/react';
4
+ import { KEYS } from 'platejs';
5
+
6
+ export const IndentKit = [
7
+ IndentPlugin.configure({
8
+ inject: {
9
+ targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote, KEYS.toggle],
10
+ },
11
+ }),
12
+ ];
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { LinkPlugin } from '@platejs/link/react';
4
+
5
+ import { LinkElement } from '@/components/ui/link-node';
6
+
7
+ export const LinkKit = [
8
+ LinkPlugin.configure({
9
+ render: {
10
+ node: LinkElement,
11
+ },
12
+ }),
13
+ ];
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import { ListPlugin } from '@platejs/list/react';
4
+ import { KEYS } from 'platejs';
5
+
6
+ import { BlockList } from '@/components/ui/block-list';
7
+
8
+ export const ListKit = [
9
+ ListPlugin.configure({
10
+ inject: {
11
+ targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote, KEYS.toggle],
12
+ },
13
+ render: {
14
+ belowNodes: BlockList,
15
+ },
16
+ }),
17
+ ];
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import { MentionInputPlugin, MentionPlugin } from '@platejs/mention/react';
4
+
5
+ import {
6
+ MentionElement,
7
+ MentionInputElement,
8
+ } from '@/components/ui/mention-node';
9
+
10
+ export const MentionKit = [
11
+ MentionPlugin.configure({
12
+ options: {
13
+ triggerPreviousCharPattern: /^$|^[\s"']$/,
14
+ },
15
+ }).withComponent(MentionElement),
16
+ MentionInputPlugin.withComponent(MentionInputElement),
17
+ ];
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { SlashInputPlugin, SlashPlugin } from '@platejs/slash-command/react';
4
+
5
+ import { SlashInputElement } from '@/components/ui/slash-node';
6
+
7
+ export const SlashKit = [
8
+ SlashPlugin.configure({
9
+ options: {
10
+ trigger: '/',
11
+ triggerPreviousCharPattern: /^\s?$/,
12
+ },
13
+ }),
14
+ SlashInputPlugin.withComponent(SlashInputElement),
15
+ ];
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ import { TogglePlugin } from '@platejs/toggle/react';
4
+
5
+ import { ToggleElement } from '@/components/ui/toggle-node';
6
+
7
+ export const ToggleKit = [TogglePlugin.withComponent(ToggleElement)];