@groupher/rich-editor 0.0.8 → 0.0.10
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 +31268 -20037
- package/dist/rich-editor.umd.js +77 -96
- package/package.json +17 -2
- 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,414 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import type { Point, TElement } from 'platejs';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ComboboxItemProps,
|
|
9
|
+
Combobox,
|
|
10
|
+
ComboboxGroup,
|
|
11
|
+
ComboboxGroupLabel,
|
|
12
|
+
ComboboxItem,
|
|
13
|
+
ComboboxPopover,
|
|
14
|
+
ComboboxProvider,
|
|
15
|
+
ComboboxRow,
|
|
16
|
+
Portal,
|
|
17
|
+
useComboboxContext,
|
|
18
|
+
useComboboxStore,
|
|
19
|
+
} from '@ariakit/react';
|
|
20
|
+
import { filterWords } from '@platejs/combobox';
|
|
21
|
+
import {
|
|
22
|
+
type UseComboboxInputResult,
|
|
23
|
+
useComboboxInput,
|
|
24
|
+
useHTMLInputCursorState,
|
|
25
|
+
} from '@platejs/combobox/react';
|
|
26
|
+
import { cva } from 'class-variance-authority';
|
|
27
|
+
import { useComposedRef, useEditorRef } from 'platejs/react';
|
|
28
|
+
|
|
29
|
+
import { cn } from '@/lib/utils';
|
|
30
|
+
|
|
31
|
+
type TFilterFn = (
|
|
32
|
+
item: { value: string; group?: string; keywords?: string[]; label?: string },
|
|
33
|
+
search: string
|
|
34
|
+
) => boolean;
|
|
35
|
+
|
|
36
|
+
type TInlineComboboxContextValue = {
|
|
37
|
+
filter: TFilterFn | false;
|
|
38
|
+
inputProps: UseComboboxInputResult['props'];
|
|
39
|
+
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
40
|
+
removeInput: UseComboboxInputResult['removeInput'];
|
|
41
|
+
showTrigger: boolean;
|
|
42
|
+
trigger: string;
|
|
43
|
+
setHasEmpty: (hasEmpty: boolean) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const InlineComboboxContext = React.createContext<TInlineComboboxContextValue>(
|
|
47
|
+
null as unknown as TInlineComboboxContextValue
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const defaultFilter: TFilterFn = (
|
|
51
|
+
{ group, keywords = [], label, value },
|
|
52
|
+
search
|
|
53
|
+
) => {
|
|
54
|
+
const uniqueTerms = new Set(
|
|
55
|
+
[value, ...keywords, group, label].filter(Boolean)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return Array.from(uniqueTerms).some((keyword) =>
|
|
59
|
+
keyword ? filterWords(keyword, search) : false
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type TInlineComboboxProps = {
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
element: TElement;
|
|
66
|
+
trigger: string;
|
|
67
|
+
filter?: TFilterFn | false;
|
|
68
|
+
hideWhenNoValue?: boolean;
|
|
69
|
+
showTrigger?: boolean;
|
|
70
|
+
value?: string;
|
|
71
|
+
setValue?: (value: string) => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const InlineCombobox = ({
|
|
75
|
+
children,
|
|
76
|
+
element,
|
|
77
|
+
filter = defaultFilter,
|
|
78
|
+
hideWhenNoValue = false,
|
|
79
|
+
setValue: setValueProp,
|
|
80
|
+
showTrigger = true,
|
|
81
|
+
trigger,
|
|
82
|
+
value: valueProp,
|
|
83
|
+
}: TInlineComboboxProps) => {
|
|
84
|
+
const editor = useEditorRef();
|
|
85
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
86
|
+
const cursorState = useHTMLInputCursorState(inputRef);
|
|
87
|
+
|
|
88
|
+
const [valueState, setValueState] = React.useState('');
|
|
89
|
+
const hasValueProp = valueProp !== undefined;
|
|
90
|
+
const value = hasValueProp ? valueProp : valueState;
|
|
91
|
+
|
|
92
|
+
const setValue = React.useCallback(
|
|
93
|
+
(newValue: string) => {
|
|
94
|
+
setValueProp?.(newValue);
|
|
95
|
+
|
|
96
|
+
if (!hasValueProp) {
|
|
97
|
+
setValueState(newValue);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[setValueProp, hasValueProp]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const insertPoint = React.useRef<Point | null>(null);
|
|
104
|
+
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
const path = editor.api.findPath(element);
|
|
107
|
+
|
|
108
|
+
if (!path) return;
|
|
109
|
+
|
|
110
|
+
const point = editor.api.before(path);
|
|
111
|
+
|
|
112
|
+
if (!point) return;
|
|
113
|
+
|
|
114
|
+
const pointRef = editor.api.pointRef(point);
|
|
115
|
+
insertPoint.current = pointRef.current;
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
pointRef.unref();
|
|
119
|
+
};
|
|
120
|
+
}, [editor, element]);
|
|
121
|
+
|
|
122
|
+
const { props: inputProps, removeInput } = useComboboxInput({
|
|
123
|
+
cancelInputOnBlur: true,
|
|
124
|
+
cursorState,
|
|
125
|
+
autoFocus: true,
|
|
126
|
+
ref: inputRef,
|
|
127
|
+
onCancelInput: (cause: string) => {
|
|
128
|
+
if (cause !== 'backspace') {
|
|
129
|
+
editor.tf.insertText(trigger + value, {
|
|
130
|
+
at: insertPoint?.current ?? undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (cause === 'arrowLeft' || cause === 'arrowRight') {
|
|
134
|
+
editor.tf.move({
|
|
135
|
+
distance: 1,
|
|
136
|
+
reverse: cause === 'arrowLeft',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const [hasEmpty, setHasEmpty] = React.useState(false);
|
|
143
|
+
|
|
144
|
+
const contextValue: TInlineComboboxContextValue = React.useMemo(
|
|
145
|
+
() => ({
|
|
146
|
+
filter,
|
|
147
|
+
inputProps,
|
|
148
|
+
inputRef,
|
|
149
|
+
removeInput,
|
|
150
|
+
setHasEmpty,
|
|
151
|
+
showTrigger,
|
|
152
|
+
trigger,
|
|
153
|
+
}),
|
|
154
|
+
[trigger, showTrigger, filter, inputProps, removeInput]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const store = useComboboxStore({
|
|
158
|
+
setValue: (newValue: string) =>
|
|
159
|
+
React.startTransition(() => setValue(newValue)),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const items = store.useState('items') as Array<{ id: string }>;
|
|
163
|
+
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
if (!items.length) return;
|
|
166
|
+
|
|
167
|
+
if (!store.getState().activeId) {
|
|
168
|
+
store.setActiveId(store.first());
|
|
169
|
+
}
|
|
170
|
+
}, [items, store]);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<span contentEditable={false}>
|
|
174
|
+
<ComboboxProvider
|
|
175
|
+
open={
|
|
176
|
+
(items.length > 0 || hasEmpty) &&
|
|
177
|
+
(!hideWhenNoValue || value.length > 0)
|
|
178
|
+
}
|
|
179
|
+
store={store}
|
|
180
|
+
>
|
|
181
|
+
<InlineComboboxContext.Provider value={contextValue}>
|
|
182
|
+
{children}
|
|
183
|
+
</InlineComboboxContext.Provider>
|
|
184
|
+
</ComboboxProvider>
|
|
185
|
+
</span>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const InlineComboboxInput = ({
|
|
190
|
+
className,
|
|
191
|
+
ref: propRef,
|
|
192
|
+
...props
|
|
193
|
+
}: React.HTMLAttributes<HTMLInputElement> & {
|
|
194
|
+
ref?: React.RefObject<HTMLInputElement | null>;
|
|
195
|
+
}) => {
|
|
196
|
+
const {
|
|
197
|
+
inputProps,
|
|
198
|
+
inputRef: contextRef,
|
|
199
|
+
showTrigger,
|
|
200
|
+
trigger,
|
|
201
|
+
} = React.useContext(InlineComboboxContext);
|
|
202
|
+
|
|
203
|
+
const store = useComboboxContext();
|
|
204
|
+
|
|
205
|
+
if (!store) return null;
|
|
206
|
+
const value = store.useState('value');
|
|
207
|
+
|
|
208
|
+
const ref = useComposedRef(propRef, contextRef);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
{showTrigger && trigger}
|
|
213
|
+
|
|
214
|
+
<span className="relative min-h-[1lh]">
|
|
215
|
+
<span
|
|
216
|
+
className="invisible overflow-hidden text-nowrap"
|
|
217
|
+
aria-hidden="true"
|
|
218
|
+
>
|
|
219
|
+
{value || '\u200B'}
|
|
220
|
+
</span>
|
|
221
|
+
|
|
222
|
+
<Combobox
|
|
223
|
+
ref={ref}
|
|
224
|
+
className={cn(
|
|
225
|
+
'absolute top-0 left-0 size-full bg-transparent outline-none',
|
|
226
|
+
className
|
|
227
|
+
)}
|
|
228
|
+
value={value}
|
|
229
|
+
autoSelect
|
|
230
|
+
{...inputProps}
|
|
231
|
+
{...props}
|
|
232
|
+
/>
|
|
233
|
+
</span>
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
InlineComboboxInput.displayName = 'InlineComboboxInput';
|
|
239
|
+
|
|
240
|
+
const InlineComboboxContent = ({
|
|
241
|
+
className,
|
|
242
|
+
...props
|
|
243
|
+
}: React.ComponentProps<typeof ComboboxPopover>) => {
|
|
244
|
+
const store = useComboboxContext();
|
|
245
|
+
|
|
246
|
+
if (!store) return null;
|
|
247
|
+
|
|
248
|
+
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
249
|
+
if (!store) return;
|
|
250
|
+
|
|
251
|
+
const state = store.getState();
|
|
252
|
+
const { items, activeId } = state as {
|
|
253
|
+
items: Array<{ id: string }>;
|
|
254
|
+
activeId?: string;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (!items.length) return;
|
|
258
|
+
|
|
259
|
+
const currentIndex = items.findIndex((item) => item.id === activeId);
|
|
260
|
+
|
|
261
|
+
if (event.key === 'ArrowUp' && currentIndex <= 0) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
store.setActiveId(store.last());
|
|
264
|
+
} else if (event.key === 'ArrowDown' && currentIndex >= items.length - 1) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
store.setActiveId(store.first());
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Portal>
|
|
272
|
+
<ComboboxPopover
|
|
273
|
+
className={cn(
|
|
274
|
+
'z-50 max-h-[260px] w-[280px] overflow-y-auto rounded-md bg-popover shadow-md',
|
|
275
|
+
className
|
|
276
|
+
)}
|
|
277
|
+
onKeyDownCapture={handleKeyDown}
|
|
278
|
+
{...props}
|
|
279
|
+
/>
|
|
280
|
+
</Portal>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const comboboxItemVariants = cva(
|
|
285
|
+
'relative mx-1 flex h-[30px] select-none items-center rounded-sm px-2 text-foreground text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
286
|
+
{
|
|
287
|
+
defaultVariants: {
|
|
288
|
+
interactive: true,
|
|
289
|
+
},
|
|
290
|
+
variants: {
|
|
291
|
+
interactive: {
|
|
292
|
+
false: '',
|
|
293
|
+
true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const InlineComboboxItem = ({
|
|
300
|
+
className,
|
|
301
|
+
focusEditor = true,
|
|
302
|
+
group,
|
|
303
|
+
keywords,
|
|
304
|
+
label,
|
|
305
|
+
onClick,
|
|
306
|
+
...props
|
|
307
|
+
}: {
|
|
308
|
+
focusEditor?: boolean;
|
|
309
|
+
group?: string;
|
|
310
|
+
keywords?: string[];
|
|
311
|
+
label?: string;
|
|
312
|
+
} & ComboboxItemProps &
|
|
313
|
+
Required<Pick<ComboboxItemProps, 'value'>>) => {
|
|
314
|
+
const { value } = props;
|
|
315
|
+
|
|
316
|
+
const { filter, removeInput } = React.useContext(InlineComboboxContext);
|
|
317
|
+
|
|
318
|
+
const store = useComboboxContext();
|
|
319
|
+
|
|
320
|
+
if (!store) return null;
|
|
321
|
+
|
|
322
|
+
const search = filter && store.useState('value');
|
|
323
|
+
|
|
324
|
+
const visible = React.useMemo(
|
|
325
|
+
() =>
|
|
326
|
+
!filter || filter({ group, keywords, label, value }, search as string),
|
|
327
|
+
[filter, group, keywords, label, value, search]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (!visible) return null;
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<ComboboxItem
|
|
334
|
+
className={cn(comboboxItemVariants(), className)}
|
|
335
|
+
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
|
336
|
+
removeInput(focusEditor);
|
|
337
|
+
onClick?.(event);
|
|
338
|
+
}}
|
|
339
|
+
{...props}
|
|
340
|
+
/>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const InlineComboboxEmpty = ({
|
|
345
|
+
children,
|
|
346
|
+
className,
|
|
347
|
+
}: React.HTMLAttributes<HTMLDivElement>) => {
|
|
348
|
+
const { setHasEmpty } = React.useContext(InlineComboboxContext);
|
|
349
|
+
const store = useComboboxContext();
|
|
350
|
+
|
|
351
|
+
if (!store) return null;
|
|
352
|
+
const items = store.useState('items');
|
|
353
|
+
|
|
354
|
+
React.useEffect(() => {
|
|
355
|
+
setHasEmpty(true);
|
|
356
|
+
|
|
357
|
+
return () => {
|
|
358
|
+
setHasEmpty(false);
|
|
359
|
+
};
|
|
360
|
+
}, [setHasEmpty]);
|
|
361
|
+
|
|
362
|
+
if (items.length > 0) return null;
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<div
|
|
366
|
+
className={cn(comboboxItemVariants({ interactive: false }), className)}
|
|
367
|
+
>
|
|
368
|
+
{children}
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const InlineComboboxRow = ComboboxRow;
|
|
374
|
+
|
|
375
|
+
function InlineComboboxGroup({
|
|
376
|
+
className,
|
|
377
|
+
...props
|
|
378
|
+
}: React.ComponentProps<typeof ComboboxGroup>) {
|
|
379
|
+
return (
|
|
380
|
+
<ComboboxGroup
|
|
381
|
+
{...props}
|
|
382
|
+
className={cn(
|
|
383
|
+
'hidden not-last:border-b py-1.5 [&:has([role=option])]:block',
|
|
384
|
+
className
|
|
385
|
+
)}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function InlineComboboxGroupLabel({
|
|
391
|
+
className,
|
|
392
|
+
...props
|
|
393
|
+
}: React.ComponentProps<typeof ComboboxGroupLabel>) {
|
|
394
|
+
return (
|
|
395
|
+
<ComboboxGroupLabel
|
|
396
|
+
{...props}
|
|
397
|
+
className={cn(
|
|
398
|
+
'mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs',
|
|
399
|
+
className
|
|
400
|
+
)}
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export {
|
|
406
|
+
InlineCombobox,
|
|
407
|
+
InlineComboboxContent,
|
|
408
|
+
InlineComboboxEmpty,
|
|
409
|
+
InlineComboboxGroup,
|
|
410
|
+
InlineComboboxGroupLabel,
|
|
411
|
+
InlineComboboxInput,
|
|
412
|
+
InlineComboboxItem,
|
|
413
|
+
InlineComboboxRow,
|
|
414
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { TLinkElement } from 'platejs';
|
|
4
|
+
import type { PlateElementProps } from 'platejs/react';
|
|
5
|
+
|
|
6
|
+
import { getLinkAttributes } from '@platejs/link';
|
|
7
|
+
import { PlateElement } from 'platejs/react';
|
|
8
|
+
|
|
9
|
+
import { cn } from '@/lib/utils';
|
|
10
|
+
|
|
11
|
+
export function LinkElement(props: PlateElementProps<TLinkElement>) {
|
|
12
|
+
return (
|
|
13
|
+
<PlateElement
|
|
14
|
+
{...props}
|
|
15
|
+
as="a"
|
|
16
|
+
className={cn(
|
|
17
|
+
'font-medium text-primary underline decoration-primary underline-offset-4',
|
|
18
|
+
props.className
|
|
19
|
+
)}
|
|
20
|
+
attributes={{
|
|
21
|
+
...props.attributes,
|
|
22
|
+
...getLinkAttributes(props.editor, props.element),
|
|
23
|
+
onMouseOver: (event) => {
|
|
24
|
+
event.stopPropagation();
|
|
25
|
+
},
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{props.children}
|
|
29
|
+
</PlateElement>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useLinkToolbarButton,
|
|
7
|
+
useLinkToolbarButtonState,
|
|
8
|
+
} from '@platejs/link/react';
|
|
9
|
+
import { Link } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { useI18n } from '@/i18n';
|
|
12
|
+
|
|
13
|
+
import { ToolbarButton } from './toolbar';
|
|
14
|
+
|
|
15
|
+
export function LinkToolbarButton({
|
|
16
|
+
tooltip,
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof ToolbarButton> & { tooltip?: string }) {
|
|
19
|
+
const state = useLinkToolbarButtonState();
|
|
20
|
+
const { props: buttonProps } = useLinkToolbarButton(state);
|
|
21
|
+
const i18n = useI18n();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ToolbarButton
|
|
25
|
+
{...props}
|
|
26
|
+
{...buttonProps}
|
|
27
|
+
data-plate-focus
|
|
28
|
+
tooltip={tooltip ?? i18n.toolbar.link}
|
|
29
|
+
>
|
|
30
|
+
<Link />
|
|
31
|
+
</ToolbarButton>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { TComboboxInputElement, TMentionElement } from "platejs";
|
|
6
|
+
import type { PlateElementProps } from "platejs/react";
|
|
7
|
+
|
|
8
|
+
import { getMentionOnSelectItem } from "@platejs/mention";
|
|
9
|
+
import { KEYS } from "platejs";
|
|
10
|
+
import {
|
|
11
|
+
PlateElement,
|
|
12
|
+
useFocused,
|
|
13
|
+
useReadOnly,
|
|
14
|
+
useSelected,
|
|
15
|
+
} from "platejs/react";
|
|
16
|
+
|
|
17
|
+
import { useI18n } from "@/i18n";
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
import { useMentionContext } from "@/mention-context";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
InlineCombobox,
|
|
23
|
+
InlineComboboxContent,
|
|
24
|
+
InlineComboboxEmpty,
|
|
25
|
+
InlineComboboxGroup,
|
|
26
|
+
InlineComboboxInput,
|
|
27
|
+
InlineComboboxItem,
|
|
28
|
+
} from "./inline-combobox";
|
|
29
|
+
|
|
30
|
+
const onSelectItem = getMentionOnSelectItem();
|
|
31
|
+
|
|
32
|
+
export function MentionElement(
|
|
33
|
+
props: PlateElementProps<TMentionElement> & {
|
|
34
|
+
prefix?: string;
|
|
35
|
+
},
|
|
36
|
+
) {
|
|
37
|
+
const element = props.element;
|
|
38
|
+
|
|
39
|
+
const selected = useSelected();
|
|
40
|
+
const focused = useFocused();
|
|
41
|
+
const readOnly = useReadOnly();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<PlateElement
|
|
45
|
+
{...props}
|
|
46
|
+
className={cn(
|
|
47
|
+
"inline-block rounded-md bg-sky-100 px-1.5 py-0.5 align-baseline font-medium text-sky-900",
|
|
48
|
+
!readOnly && "cursor-pointer",
|
|
49
|
+
selected && focused && "ring-2 ring-ring",
|
|
50
|
+
element.children[0][KEYS.bold] === true && "font-bold",
|
|
51
|
+
element.children[0][KEYS.italic] === true && "italic",
|
|
52
|
+
element.children[0][KEYS.underline] === true && "underline",
|
|
53
|
+
)}
|
|
54
|
+
attributes={{
|
|
55
|
+
...props.attributes,
|
|
56
|
+
contentEditable: false,
|
|
57
|
+
"data-slate-value": element.value,
|
|
58
|
+
draggable: true,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<>
|
|
62
|
+
@{element.value}
|
|
63
|
+
{props.children}
|
|
64
|
+
</>
|
|
65
|
+
</PlateElement>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function MentionInputElement(
|
|
70
|
+
props: PlateElementProps<TComboboxInputElement>,
|
|
71
|
+
) {
|
|
72
|
+
const { editor, element } = props;
|
|
73
|
+
const [search, setSearch] = useState("");
|
|
74
|
+
const i18n = useI18n();
|
|
75
|
+
const { mentionOptions: mentionablesProp, onMentionSearch } =
|
|
76
|
+
useMentionContext();
|
|
77
|
+
|
|
78
|
+
const mentionOptions = useMemo(
|
|
79
|
+
() => mentionablesProp ?? [],
|
|
80
|
+
[mentionablesProp],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
onMentionSearch?.(search);
|
|
85
|
+
}, [onMentionSearch, search]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<PlateElement {...props} as="span">
|
|
89
|
+
<InlineCombobox
|
|
90
|
+
value={search}
|
|
91
|
+
element={element}
|
|
92
|
+
setValue={setSearch}
|
|
93
|
+
showTrigger={false}
|
|
94
|
+
trigger="@"
|
|
95
|
+
filter={({ value }, query) => {
|
|
96
|
+
if (query.length === 0) return true;
|
|
97
|
+
|
|
98
|
+
return value.toLowerCase().includes(query.toLowerCase());
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<span className="inline-block rounded-md bg-sky-100 px-1.5 py-0.5 align-baseline text-sm text-sky-900 ring-ring focus-within:ring-2">
|
|
102
|
+
@
|
|
103
|
+
<InlineComboboxInput />
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
<InlineComboboxContent className="my-1.5">
|
|
107
|
+
<InlineComboboxEmpty>{i18n.mention.empty}</InlineComboboxEmpty>
|
|
108
|
+
|
|
109
|
+
<InlineComboboxGroup>
|
|
110
|
+
{mentionOptions.map((item) => (
|
|
111
|
+
<InlineComboboxItem
|
|
112
|
+
key={item.key}
|
|
113
|
+
value={item.text}
|
|
114
|
+
onClick={() => onSelectItem(editor, item, search)}
|
|
115
|
+
>
|
|
116
|
+
{item.text}
|
|
117
|
+
</InlineComboboxItem>
|
|
118
|
+
))}
|
|
119
|
+
</InlineComboboxGroup>
|
|
120
|
+
</InlineComboboxContent>
|
|
121
|
+
</InlineCombobox>
|
|
122
|
+
|
|
123
|
+
{props.children}
|
|
124
|
+
</PlateElement>
|
|
125
|
+
);
|
|
126
|
+
}
|