@groupher/rich-editor 0.0.7 → 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.
- package/dist/rich-editor.es.js +33455 -26238
- package/dist/rich-editor.umd.js +77 -101
- package/package.json +21 -6
- package/src/RichEditor.tsx +204 -92
- package/src/components/editor/editor-kit.tsx +27 -0
- package/src/components/editor/plugins/autoformat-kit.tsx +99 -0
- package/src/components/editor/plugins/callout-kit.tsx +7 -0
- package/src/components/editor/plugins/emoji-kit.tsx +14 -0
- package/src/components/editor/plugins/indent-kit.tsx +12 -0
- package/src/components/editor/plugins/link-kit.tsx +13 -0
- package/src/components/editor/plugins/list-kit.tsx +17 -0
- package/src/components/editor/plugins/mention-kit.tsx +17 -0
- package/src/components/editor/plugins/slash-kit.tsx +15 -0
- package/src/components/editor/plugins/toggle-kit.tsx +7 -0
- package/src/components/ui/action-bar.tsx +208 -0
- package/src/components/ui/block-list.tsx +94 -0
- package/src/components/ui/button.tsx +49 -50
- package/src/components/ui/callout-node.tsx +65 -0
- package/src/components/ui/editor-static.tsx +44 -44
- package/src/components/ui/editor.tsx +107 -107
- package/src/components/ui/emoji-node.tsx +71 -0
- package/src/components/ui/emoji-toolbar-button.tsx +618 -0
- package/src/components/ui/floating-toolbar.tsx +86 -0
- package/src/components/ui/inline-combobox.tsx +414 -0
- package/src/components/ui/link-node.tsx +31 -0
- package/src/components/ui/link-toolbar-button.tsx +33 -0
- package/src/components/ui/mention-node.tsx +126 -0
- package/src/components/ui/slash-node.tsx +191 -0
- package/src/components/ui/toggle-node.tsx +36 -0
- package/src/components/ui/toolbar.tsx +10 -10
- package/src/hooks/use-debounce.ts +15 -0
- package/src/hooks/use-mounted.ts +11 -0
- package/src/i18n.tsx +155 -0
- package/src/main.tsx +35 -14
- package/src/mention-context.tsx +32 -0
- package/src/vite-env.d.ts +7 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import type { PlateEditor, PlateElementProps } from 'platejs/react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ChevronRightIcon,
|
|
9
|
+
Heading1Icon,
|
|
10
|
+
Heading2Icon,
|
|
11
|
+
Heading3Icon,
|
|
12
|
+
LightbulbIcon,
|
|
13
|
+
ListIcon,
|
|
14
|
+
ListOrdered,
|
|
15
|
+
PilcrowIcon,
|
|
16
|
+
Quote,
|
|
17
|
+
Square,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { type TComboboxInputElement, type TElement, KEYS } from 'platejs';
|
|
20
|
+
import { ListStyleType } from '@platejs/list';
|
|
21
|
+
import { PlateElement } from 'platejs/react';
|
|
22
|
+
|
|
23
|
+
import { useI18n } from '@/i18n';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
InlineCombobox,
|
|
27
|
+
InlineComboboxContent,
|
|
28
|
+
InlineComboboxEmpty,
|
|
29
|
+
InlineComboboxGroup,
|
|
30
|
+
InlineComboboxGroupLabel,
|
|
31
|
+
InlineComboboxInput,
|
|
32
|
+
InlineComboboxItem,
|
|
33
|
+
} from './inline-combobox';
|
|
34
|
+
|
|
35
|
+
type TGroupItem = {
|
|
36
|
+
icon: React.ReactNode;
|
|
37
|
+
value: string;
|
|
38
|
+
keywords?: string[];
|
|
39
|
+
label: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const listStyleMap: Record<string, ListStyleType> = {
|
|
43
|
+
[KEYS.ul]: ListStyleType.Disc,
|
|
44
|
+
[KEYS.ol]: ListStyleType.Decimal,
|
|
45
|
+
[KEYS.listTodo]: 'todo' as ListStyleType,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const listTypes = new Set(Object.keys(listStyleMap));
|
|
49
|
+
|
|
50
|
+
const setBlockType = (editor: PlateEditor, type: string) => {
|
|
51
|
+
editor.tf.withoutNormalizing(() => {
|
|
52
|
+
const entries = editor.api.blocks({ mode: 'lowest' });
|
|
53
|
+
|
|
54
|
+
for (const [node, path] of entries) {
|
|
55
|
+
if (listTypes.has(type)) {
|
|
56
|
+
editor.tf.setNodes(
|
|
57
|
+
{
|
|
58
|
+
indent: 1,
|
|
59
|
+
listStyleType: listStyleMap[type],
|
|
60
|
+
},
|
|
61
|
+
{ at: path }
|
|
62
|
+
);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ((node as TElement)[KEYS.listType]) {
|
|
67
|
+
editor.tf.unsetNodes([KEYS.listType, 'indent'], { at: path });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
editor.tf.setNodes({ type }, { at: path });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function SlashInputElement(
|
|
76
|
+
props: PlateElementProps<TComboboxInputElement>
|
|
77
|
+
) {
|
|
78
|
+
const { editor, element } = props;
|
|
79
|
+
const i18n = useI18n();
|
|
80
|
+
|
|
81
|
+
const groups = React.useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
group: i18n.slash.groups.blocks,
|
|
86
|
+
items: [
|
|
87
|
+
{
|
|
88
|
+
icon: <PilcrowIcon />,
|
|
89
|
+
keywords: ['paragraph'],
|
|
90
|
+
label: i18n.slash.items.paragraph,
|
|
91
|
+
value: KEYS.p,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
icon: <Heading1Icon />,
|
|
95
|
+
keywords: ['h1'],
|
|
96
|
+
label: i18n.slash.items.heading1,
|
|
97
|
+
value: KEYS.h1,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
icon: <Heading2Icon />,
|
|
101
|
+
keywords: ['h2'],
|
|
102
|
+
label: i18n.slash.items.heading2,
|
|
103
|
+
value: KEYS.h2,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
icon: <Heading3Icon />,
|
|
107
|
+
keywords: ['h3'],
|
|
108
|
+
label: i18n.slash.items.heading3,
|
|
109
|
+
value: KEYS.h3,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
icon: <ChevronRightIcon />,
|
|
113
|
+
keywords: ['toggle'],
|
|
114
|
+
label: i18n.slash.items.toggle,
|
|
115
|
+
value: KEYS.toggle,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
icon: <LightbulbIcon />,
|
|
119
|
+
keywords: ['callout'],
|
|
120
|
+
label: i18n.slash.items.callout,
|
|
121
|
+
value: KEYS.callout,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
icon: <Quote />,
|
|
125
|
+
keywords: ['quote', 'blockquote'],
|
|
126
|
+
label: i18n.slash.items.blockquote,
|
|
127
|
+
value: KEYS.blockquote,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
group: i18n.slash.groups.lists,
|
|
133
|
+
items: [
|
|
134
|
+
{
|
|
135
|
+
icon: <ListIcon />,
|
|
136
|
+
keywords: ['unordered', 'ul', '-'],
|
|
137
|
+
label: i18n.slash.items.bulletedList,
|
|
138
|
+
value: KEYS.ul,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
icon: <ListOrdered />,
|
|
142
|
+
keywords: ['ordered', 'ol', '1'],
|
|
143
|
+
label: i18n.slash.items.numberedList,
|
|
144
|
+
value: KEYS.ol,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
icon: <Square />,
|
|
148
|
+
keywords: ['todo', 'task', 'checkbox'],
|
|
149
|
+
label: i18n.slash.items.todoList,
|
|
150
|
+
value: KEYS.listTodo,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
] as Array<{ group: string; items: TGroupItem[] }>,
|
|
155
|
+
[i18n]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<PlateElement {...props} as="span">
|
|
160
|
+
<InlineCombobox element={element} trigger="/">
|
|
161
|
+
<InlineComboboxInput />
|
|
162
|
+
|
|
163
|
+
<InlineComboboxContent>
|
|
164
|
+
<InlineComboboxEmpty>{i18n.slash.empty}</InlineComboboxEmpty>
|
|
165
|
+
|
|
166
|
+
{groups.map(({ group, items }) => (
|
|
167
|
+
<InlineComboboxGroup key={group}>
|
|
168
|
+
<InlineComboboxGroupLabel>{group}</InlineComboboxGroupLabel>
|
|
169
|
+
|
|
170
|
+
{items.map(({ icon, keywords, label, value }) => (
|
|
171
|
+
<InlineComboboxItem
|
|
172
|
+
key={value}
|
|
173
|
+
value={value}
|
|
174
|
+
onClick={() => setBlockType(editor, value)}
|
|
175
|
+
label={label}
|
|
176
|
+
group={group}
|
|
177
|
+
keywords={keywords}
|
|
178
|
+
>
|
|
179
|
+
<div className="mr-2 text-muted-foreground">{icon}</div>
|
|
180
|
+
{label}
|
|
181
|
+
</InlineComboboxItem>
|
|
182
|
+
))}
|
|
183
|
+
</InlineComboboxGroup>
|
|
184
|
+
))}
|
|
185
|
+
</InlineComboboxContent>
|
|
186
|
+
</InlineCombobox>
|
|
187
|
+
|
|
188
|
+
{props.children}
|
|
189
|
+
</PlateElement>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { PlateElementProps } from 'platejs/react';
|
|
4
|
+
|
|
5
|
+
import { useToggleButton, useToggleButtonState } from '@platejs/toggle/react';
|
|
6
|
+
import { ChevronRight } from 'lucide-react';
|
|
7
|
+
import { PlateElement } from 'platejs/react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '@/components/ui/button';
|
|
10
|
+
|
|
11
|
+
export function ToggleElement(props: PlateElementProps) {
|
|
12
|
+
const element = props.element;
|
|
13
|
+
const state = useToggleButtonState(element.id as string);
|
|
14
|
+
const { buttonProps, open } = useToggleButton(state);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<PlateElement {...props} className="pl-6">
|
|
18
|
+
<Button
|
|
19
|
+
size="icon"
|
|
20
|
+
variant="ghost"
|
|
21
|
+
className="-left-0.5 absolute top-0 size-6 cursor-pointer select-none items-center justify-center rounded-md p-px text-muted-foreground transition-colors hover:bg-accent [&_svg]:size-4"
|
|
22
|
+
contentEditable={false}
|
|
23
|
+
{...buttonProps}
|
|
24
|
+
>
|
|
25
|
+
<ChevronRight
|
|
26
|
+
className={
|
|
27
|
+
open
|
|
28
|
+
? 'rotate-90 transition-transform duration-75'
|
|
29
|
+
: 'rotate-0 transition-transform duration-75'
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
</Button>
|
|
33
|
+
{props.children}
|
|
34
|
+
</PlateElement>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -112,7 +112,7 @@ const dropdownArrowVariants = cva(
|
|
|
112
112
|
}
|
|
113
113
|
);
|
|
114
114
|
|
|
115
|
-
type
|
|
115
|
+
type TToolbarButtonProps = {
|
|
116
116
|
isDropdown?: boolean;
|
|
117
117
|
pressed?: boolean;
|
|
118
118
|
} & Omit<
|
|
@@ -129,7 +129,7 @@ export const ToolbarButton = withTooltip(function ToolbarButton({
|
|
|
129
129
|
size = 'sm',
|
|
130
130
|
variant,
|
|
131
131
|
...props
|
|
132
|
-
}:
|
|
132
|
+
}: TToolbarButtonProps) {
|
|
133
133
|
return typeof pressed === 'boolean' ? (
|
|
134
134
|
<ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
|
|
135
135
|
<ToolbarToggleItem
|
|
@@ -190,7 +190,7 @@ export function ToolbarSplitButton({
|
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
type
|
|
193
|
+
type TToolbarSplitButtonPrimaryProps = Omit<
|
|
194
194
|
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
|
|
195
195
|
'value'
|
|
196
196
|
> &
|
|
@@ -202,7 +202,7 @@ export function ToolbarSplitButtonPrimary({
|
|
|
202
202
|
size = 'sm',
|
|
203
203
|
variant,
|
|
204
204
|
...props
|
|
205
|
-
}:
|
|
205
|
+
}: TToolbarSplitButtonPrimaryProps) {
|
|
206
206
|
return (
|
|
207
207
|
<span
|
|
208
208
|
className={cn(
|
|
@@ -226,10 +226,11 @@ export function ToolbarSplitButtonSecondary({
|
|
|
226
226
|
size,
|
|
227
227
|
variant,
|
|
228
228
|
...props
|
|
229
|
-
}: React.ComponentPropsWithoutRef<'
|
|
229
|
+
}: React.ComponentPropsWithoutRef<'button'> &
|
|
230
230
|
VariantProps<typeof dropdownArrowVariants>) {
|
|
231
231
|
return (
|
|
232
|
-
<
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
233
234
|
className={cn(
|
|
234
235
|
dropdownArrowVariants({
|
|
235
236
|
size,
|
|
@@ -239,11 +240,10 @@ export function ToolbarSplitButtonSecondary({
|
|
|
239
240
|
className
|
|
240
241
|
)}
|
|
241
242
|
onClick={(e) => e.stopPropagation()}
|
|
242
|
-
role="button"
|
|
243
243
|
{...props}
|
|
244
244
|
>
|
|
245
245
|
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
|
|
246
|
-
</
|
|
246
|
+
</button>
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -283,7 +283,7 @@ export function ToolbarGroup({
|
|
|
283
283
|
);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
type
|
|
286
|
+
type TTooltipProps<T extends React.ElementType> = {
|
|
287
287
|
tooltip?: React.ReactNode;
|
|
288
288
|
tooltipContentProps?: Omit<
|
|
289
289
|
React.ComponentPropsWithoutRef<typeof TooltipContent>,
|
|
@@ -303,7 +303,7 @@ function withTooltip<T extends React.ElementType>(Component: T) {
|
|
|
303
303
|
tooltipProps,
|
|
304
304
|
tooltipTriggerProps,
|
|
305
305
|
...props
|
|
306
|
-
}:
|
|
306
|
+
}: TTooltipProps<T>) {
|
|
307
307
|
const [mounted, setMounted] = React.useState(false);
|
|
308
308
|
|
|
309
309
|
React.useEffect(() => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delay = 200) {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = React.useState(value);
|
|
5
|
+
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
const timer = setTimeout(() => {
|
|
8
|
+
setDebouncedValue(value);
|
|
9
|
+
}, delay);
|
|
10
|
+
|
|
11
|
+
return () => clearTimeout(timer);
|
|
12
|
+
}, [delay, value]);
|
|
13
|
+
|
|
14
|
+
return debouncedValue;
|
|
15
|
+
}
|
package/src/i18n.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export type TLocale = 'en' | 'zh-CN';
|
|
4
|
+
|
|
5
|
+
type TI18nStrings = {
|
|
6
|
+
locale: TLocale;
|
|
7
|
+
placeholder: string;
|
|
8
|
+
toolbar: {
|
|
9
|
+
bold: string;
|
|
10
|
+
italic: string;
|
|
11
|
+
underline: string;
|
|
12
|
+
strikethrough: string;
|
|
13
|
+
link: string;
|
|
14
|
+
};
|
|
15
|
+
slash: {
|
|
16
|
+
empty: string;
|
|
17
|
+
groups: {
|
|
18
|
+
blocks: string;
|
|
19
|
+
lists: string;
|
|
20
|
+
};
|
|
21
|
+
items: {
|
|
22
|
+
paragraph: string;
|
|
23
|
+
heading1: string;
|
|
24
|
+
heading2: string;
|
|
25
|
+
heading3: string;
|
|
26
|
+
bulletedList: string;
|
|
27
|
+
numberedList: string;
|
|
28
|
+
todoList: string;
|
|
29
|
+
toggle: string;
|
|
30
|
+
callout: string;
|
|
31
|
+
blockquote: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
mention: {
|
|
35
|
+
empty: string;
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
export: {
|
|
39
|
+
title: string;
|
|
40
|
+
button: string;
|
|
41
|
+
placeholder: string;
|
|
42
|
+
loadButton: string;
|
|
43
|
+
readonlyTitle: string;
|
|
44
|
+
invalidJson: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const translations: Record<TLocale, TI18nStrings> = {
|
|
49
|
+
en: {
|
|
50
|
+
locale: 'en',
|
|
51
|
+
placeholder: 'Type your content here...',
|
|
52
|
+
toolbar: {
|
|
53
|
+
bold: 'Bold',
|
|
54
|
+
italic: 'Italic',
|
|
55
|
+
underline: 'Underline',
|
|
56
|
+
strikethrough: 'Strikethrough',
|
|
57
|
+
link: 'Link',
|
|
58
|
+
},
|
|
59
|
+
slash: {
|
|
60
|
+
empty: 'No results',
|
|
61
|
+
groups: {
|
|
62
|
+
blocks: 'Blocks',
|
|
63
|
+
lists: 'Lists',
|
|
64
|
+
},
|
|
65
|
+
items: {
|
|
66
|
+
paragraph: 'Text',
|
|
67
|
+
heading1: 'Heading 1',
|
|
68
|
+
heading2: 'Heading 2',
|
|
69
|
+
heading3: 'Heading 3',
|
|
70
|
+
bulletedList: 'Bulleted list',
|
|
71
|
+
numberedList: 'Numbered list',
|
|
72
|
+
todoList: 'To-do list',
|
|
73
|
+
toggle: 'Toggle',
|
|
74
|
+
callout: 'Callout',
|
|
75
|
+
blockquote: 'Blockquote',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
mention: {
|
|
79
|
+
empty: 'No matches',
|
|
80
|
+
label: 'Mention',
|
|
81
|
+
},
|
|
82
|
+
export: {
|
|
83
|
+
title: 'Export JSON',
|
|
84
|
+
button: 'Export',
|
|
85
|
+
placeholder: 'Paste exported JSON here to preview read-only.',
|
|
86
|
+
loadButton: 'Render Read-only',
|
|
87
|
+
readonlyTitle: 'Read-only Preview',
|
|
88
|
+
invalidJson: 'Invalid JSON, please check formatting.',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
'zh-CN': {
|
|
92
|
+
locale: 'zh-CN',
|
|
93
|
+
placeholder: '输入内容…',
|
|
94
|
+
toolbar: {
|
|
95
|
+
bold: '加粗',
|
|
96
|
+
italic: '斜体',
|
|
97
|
+
underline: '下划线',
|
|
98
|
+
strikethrough: '删除线',
|
|
99
|
+
link: '链接',
|
|
100
|
+
},
|
|
101
|
+
slash: {
|
|
102
|
+
empty: '没有匹配结果',
|
|
103
|
+
groups: {
|
|
104
|
+
blocks: '块级',
|
|
105
|
+
lists: '列表',
|
|
106
|
+
},
|
|
107
|
+
items: {
|
|
108
|
+
paragraph: '文本',
|
|
109
|
+
heading1: '标题 1',
|
|
110
|
+
heading2: '标题 2',
|
|
111
|
+
heading3: '标题 3',
|
|
112
|
+
bulletedList: '无序列表',
|
|
113
|
+
numberedList: '有序列表',
|
|
114
|
+
todoList: '待办列表',
|
|
115
|
+
toggle: '折叠',
|
|
116
|
+
callout: '提示块',
|
|
117
|
+
blockquote: '引用',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
mention: {
|
|
121
|
+
empty: '没有匹配用户',
|
|
122
|
+
label: '提及',
|
|
123
|
+
},
|
|
124
|
+
export: {
|
|
125
|
+
title: '导出 JSON',
|
|
126
|
+
button: '导出',
|
|
127
|
+
placeholder: '粘贴导出的 JSON 以预览只读内容。',
|
|
128
|
+
loadButton: '渲染只读',
|
|
129
|
+
readonlyTitle: '只读预览',
|
|
130
|
+
invalidJson: 'JSON 格式错误,请检查。',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const defaultLocale: TLocale = 'zh-CN';
|
|
136
|
+
|
|
137
|
+
const I18nContext = React.createContext<TI18nStrings>(
|
|
138
|
+
translations[defaultLocale]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export function I18nProvider({
|
|
142
|
+
children,
|
|
143
|
+
locale = defaultLocale,
|
|
144
|
+
}: {
|
|
145
|
+
children: React.ReactNode;
|
|
146
|
+
locale?: TLocale;
|
|
147
|
+
}) {
|
|
148
|
+
const value = translations[locale] ?? translations[defaultLocale];
|
|
149
|
+
|
|
150
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function useI18n() {
|
|
154
|
+
return React.useContext(I18nContext);
|
|
155
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import { StrictMode } from
|
|
2
|
-
import { createRoot } from
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import RE from
|
|
6
|
-
import RichEditor from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
|
|
4
|
+
import "./global.css";
|
|
5
|
+
import RE from "@groupher/rich-editor";
|
|
6
|
+
import RichEditor from "./RichEditor.tsx";
|
|
7
|
+
|
|
8
|
+
const MENTION_OPTIONS = [
|
|
9
|
+
{
|
|
10
|
+
key: "0",
|
|
11
|
+
text: "Alice",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "1",
|
|
15
|
+
text: "Bob",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "2",
|
|
19
|
+
text: "Simon",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const rootElement = document.getElementById("root");
|
|
24
|
+
|
|
25
|
+
if (!rootElement) {
|
|
26
|
+
throw new Error("Root element not found");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
createRoot(rootElement).render(
|
|
30
|
+
<StrictMode>
|
|
31
|
+
<RichEditor mentionOptions={MENTION_OPTIONS} />
|
|
32
|
+
<hr />
|
|
33
|
+
<RE />
|
|
34
|
+
</StrictMode>,
|
|
35
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type TMentionOption = {
|
|
4
|
+
key: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type TMentionContextValue = {
|
|
9
|
+
mentionOptions?: TMentionOption[];
|
|
10
|
+
onMentionSearch?: (query: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const MentionContext = React.createContext<TMentionContextValue>({});
|
|
14
|
+
|
|
15
|
+
export function MentionProvider({
|
|
16
|
+
children,
|
|
17
|
+
mentionOptions,
|
|
18
|
+
onMentionSearch,
|
|
19
|
+
}: React.PropsWithChildren<TMentionContextValue>) {
|
|
20
|
+
const value = React.useMemo(
|
|
21
|
+
() => ({ mentionOptions, onMentionSearch }),
|
|
22
|
+
[mentionOptions, onMentionSearch],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<MentionContext.Provider value={value}>{children}</MentionContext.Provider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useMentionContext() {
|
|
31
|
+
return React.useContext(MentionContext);
|
|
32
|
+
}
|