@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,618 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/* eslint-disable react-hooks/refs */
|
|
3
|
+
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import type { Emoji } from '@emoji-mart/data';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type EmojiCategoryList,
|
|
10
|
+
type EmojiIconList,
|
|
11
|
+
type GridRow,
|
|
12
|
+
EmojiSettings,
|
|
13
|
+
} from '@platejs/emoji';
|
|
14
|
+
import {
|
|
15
|
+
type EmojiDropdownMenuOptions,
|
|
16
|
+
type UseEmojiPickerType,
|
|
17
|
+
useEmojiDropdownMenuState,
|
|
18
|
+
} from '@platejs/emoji/react';
|
|
19
|
+
import * as Popover from '@radix-ui/react-popover';
|
|
20
|
+
import {
|
|
21
|
+
AppleIcon,
|
|
22
|
+
ClockIcon,
|
|
23
|
+
CompassIcon,
|
|
24
|
+
FlagIcon,
|
|
25
|
+
LeafIcon,
|
|
26
|
+
LightbulbIcon,
|
|
27
|
+
MusicIcon,
|
|
28
|
+
SearchIcon,
|
|
29
|
+
SmileIcon,
|
|
30
|
+
StarIcon,
|
|
31
|
+
XIcon,
|
|
32
|
+
} from 'lucide-react';
|
|
33
|
+
|
|
34
|
+
import { Button } from '@/components/ui/button';
|
|
35
|
+
import {
|
|
36
|
+
Tooltip,
|
|
37
|
+
TooltipContent,
|
|
38
|
+
TooltipProvider,
|
|
39
|
+
TooltipTrigger,
|
|
40
|
+
} from '@/components/ui/tooltip';
|
|
41
|
+
import { cn } from '@/lib/utils';
|
|
42
|
+
|
|
43
|
+
import { ToolbarButton } from './toolbar';
|
|
44
|
+
|
|
45
|
+
export function EmojiToolbarButton({
|
|
46
|
+
options,
|
|
47
|
+
...props
|
|
48
|
+
}: {
|
|
49
|
+
options?: EmojiDropdownMenuOptions;
|
|
50
|
+
} & React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
|
|
51
|
+
const { emojiPickerState, isOpen, setIsOpen } =
|
|
52
|
+
useEmojiDropdownMenuState(options);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<EmojiPopover
|
|
56
|
+
control={
|
|
57
|
+
<ToolbarButton pressed={isOpen} tooltip="Emoji" isDropdown {...props}>
|
|
58
|
+
<SmileIcon />
|
|
59
|
+
</ToolbarButton>
|
|
60
|
+
}
|
|
61
|
+
isOpen={isOpen}
|
|
62
|
+
setIsOpen={setIsOpen}
|
|
63
|
+
>
|
|
64
|
+
<EmojiPicker
|
|
65
|
+
{...emojiPickerState}
|
|
66
|
+
isOpen={isOpen}
|
|
67
|
+
setIsOpen={setIsOpen}
|
|
68
|
+
settings={options?.settings}
|
|
69
|
+
/>
|
|
70
|
+
</EmojiPopover>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function EmojiPopover({
|
|
75
|
+
children,
|
|
76
|
+
control,
|
|
77
|
+
isOpen,
|
|
78
|
+
setIsOpen,
|
|
79
|
+
}: {
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
control: React.ReactNode;
|
|
82
|
+
isOpen: boolean;
|
|
83
|
+
setIsOpen: (open: boolean) => void;
|
|
84
|
+
}) {
|
|
85
|
+
return (
|
|
86
|
+
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
87
|
+
<Popover.Trigger asChild>{control}</Popover.Trigger>
|
|
88
|
+
|
|
89
|
+
<Popover.Portal>
|
|
90
|
+
<Popover.Content className="z-100">{children}</Popover.Content>
|
|
91
|
+
</Popover.Portal>
|
|
92
|
+
</Popover.Root>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function EmojiPicker({
|
|
97
|
+
clearSearch,
|
|
98
|
+
emoji,
|
|
99
|
+
emojiLibrary,
|
|
100
|
+
focusedCategory,
|
|
101
|
+
hasFound,
|
|
102
|
+
i18n,
|
|
103
|
+
icons = {
|
|
104
|
+
categories: emojiCategoryIcons,
|
|
105
|
+
search: emojiSearchIcons,
|
|
106
|
+
},
|
|
107
|
+
isSearching,
|
|
108
|
+
refs,
|
|
109
|
+
searchResult,
|
|
110
|
+
searchValue,
|
|
111
|
+
setSearch,
|
|
112
|
+
settings = EmojiSettings,
|
|
113
|
+
visibleCategories,
|
|
114
|
+
handleCategoryClick,
|
|
115
|
+
onMouseOver,
|
|
116
|
+
onSelectEmoji,
|
|
117
|
+
}: Omit<UseEmojiPickerType, 'icons'> & {
|
|
118
|
+
icons?: EmojiIconList<React.ReactElement>;
|
|
119
|
+
}) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
'flex h-[23rem] w-80 flex-col rounded-xl bg-popover text-popover-foreground',
|
|
124
|
+
'border shadow-md'
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<EmojiPickerNavigation
|
|
128
|
+
onClick={handleCategoryClick}
|
|
129
|
+
emojiLibrary={emojiLibrary}
|
|
130
|
+
focusedCategory={focusedCategory}
|
|
131
|
+
i18n={i18n}
|
|
132
|
+
icons={icons}
|
|
133
|
+
/>
|
|
134
|
+
<EmojiPickerSearchBar
|
|
135
|
+
i18n={i18n}
|
|
136
|
+
searchValue={searchValue}
|
|
137
|
+
setSearch={setSearch}
|
|
138
|
+
>
|
|
139
|
+
<EmojiPickerSearchAndClear
|
|
140
|
+
clearSearch={clearSearch}
|
|
141
|
+
i18n={i18n}
|
|
142
|
+
searchValue={searchValue}
|
|
143
|
+
/>
|
|
144
|
+
</EmojiPickerSearchBar>
|
|
145
|
+
<EmojiPickerContent
|
|
146
|
+
onMouseOver={onMouseOver}
|
|
147
|
+
onSelectEmoji={onSelectEmoji}
|
|
148
|
+
emojiLibrary={emojiLibrary}
|
|
149
|
+
i18n={i18n}
|
|
150
|
+
isSearching={isSearching}
|
|
151
|
+
refs={refs}
|
|
152
|
+
searchResult={searchResult}
|
|
153
|
+
settings={settings}
|
|
154
|
+
visibleCategories={visibleCategories}
|
|
155
|
+
/>
|
|
156
|
+
<EmojiPickerPreview
|
|
157
|
+
emoji={emoji}
|
|
158
|
+
hasFound={hasFound}
|
|
159
|
+
i18n={i18n}
|
|
160
|
+
isSearching={isSearching}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const EmojiButton = React.memo(function EmojiButton({
|
|
167
|
+
emoji,
|
|
168
|
+
index,
|
|
169
|
+
onMouseOver,
|
|
170
|
+
onSelect,
|
|
171
|
+
}: {
|
|
172
|
+
emoji: Emoji;
|
|
173
|
+
index: number;
|
|
174
|
+
onMouseOver: (emoji?: Emoji) => void;
|
|
175
|
+
onSelect: (emoji: Emoji) => void;
|
|
176
|
+
}) {
|
|
177
|
+
return (
|
|
178
|
+
<button
|
|
179
|
+
className="group relative flex size-9 cursor-pointer items-center justify-center border-none bg-transparent text-2xl leading-none"
|
|
180
|
+
onClick={() => onSelect(emoji)}
|
|
181
|
+
onMouseEnter={() => onMouseOver(emoji)}
|
|
182
|
+
onMouseLeave={() => onMouseOver()}
|
|
183
|
+
aria-label={emoji.skins[0].native}
|
|
184
|
+
data-index={index}
|
|
185
|
+
tabIndex={-1}
|
|
186
|
+
type="button"
|
|
187
|
+
>
|
|
188
|
+
<div
|
|
189
|
+
className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100"
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
/>
|
|
192
|
+
<span
|
|
193
|
+
className="relative"
|
|
194
|
+
style={{
|
|
195
|
+
fontFamily:
|
|
196
|
+
'"Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols',
|
|
197
|
+
}}
|
|
198
|
+
data-emoji-set="native"
|
|
199
|
+
>
|
|
200
|
+
{emoji.skins[0].native}
|
|
201
|
+
</span>
|
|
202
|
+
</button>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const RowOfButtons = React.memo(function RowOfButtons({
|
|
207
|
+
emojiLibrary,
|
|
208
|
+
row,
|
|
209
|
+
onMouseOver,
|
|
210
|
+
onSelectEmoji,
|
|
211
|
+
}: {
|
|
212
|
+
row: GridRow;
|
|
213
|
+
} & Pick<
|
|
214
|
+
UseEmojiPickerType,
|
|
215
|
+
'emojiLibrary' | 'onMouseOver' | 'onSelectEmoji'
|
|
216
|
+
>) {
|
|
217
|
+
return (
|
|
218
|
+
<div key={row.id} className="flex" data-index={row.id}>
|
|
219
|
+
{row.elements.map((emojiId: string, index: number) => (
|
|
220
|
+
<EmojiButton
|
|
221
|
+
key={emojiId}
|
|
222
|
+
onMouseOver={onMouseOver}
|
|
223
|
+
onSelect={onSelectEmoji}
|
|
224
|
+
emoji={emojiLibrary.getEmoji(emojiId)}
|
|
225
|
+
index={index}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
function EmojiPickerContent({
|
|
233
|
+
emojiLibrary,
|
|
234
|
+
i18n,
|
|
235
|
+
isSearching = false,
|
|
236
|
+
refs,
|
|
237
|
+
searchResult,
|
|
238
|
+
settings = EmojiSettings,
|
|
239
|
+
visibleCategories,
|
|
240
|
+
onMouseOver,
|
|
241
|
+
onSelectEmoji,
|
|
242
|
+
}: Pick<
|
|
243
|
+
UseEmojiPickerType,
|
|
244
|
+
| 'emojiLibrary'
|
|
245
|
+
| 'i18n'
|
|
246
|
+
| 'isSearching'
|
|
247
|
+
| 'onMouseOver'
|
|
248
|
+
| 'onSelectEmoji'
|
|
249
|
+
| 'refs'
|
|
250
|
+
| 'searchResult'
|
|
251
|
+
| 'settings'
|
|
252
|
+
| 'visibleCategories'
|
|
253
|
+
>) {
|
|
254
|
+
const getRowWidth = settings.perLine.value * settings.buttonSize.value;
|
|
255
|
+
|
|
256
|
+
const isCategoryVisible = React.useCallback(
|
|
257
|
+
(categoryId: EmojiCategoryList) =>
|
|
258
|
+
visibleCategories.has(categoryId)
|
|
259
|
+
? visibleCategories.get(categoryId)
|
|
260
|
+
: false,
|
|
261
|
+
[visibleCategories]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const EmojiList = React.useCallback(
|
|
265
|
+
() =>
|
|
266
|
+
emojiLibrary
|
|
267
|
+
.getGrid()
|
|
268
|
+
.sections()
|
|
269
|
+
.map(({ id: categoryId }: { id: EmojiCategoryList }) => {
|
|
270
|
+
const section = emojiLibrary.getGrid().section(categoryId);
|
|
271
|
+
const { buttonSize } = settings;
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
key={categoryId}
|
|
276
|
+
ref={section.root}
|
|
277
|
+
style={{ width: getRowWidth }}
|
|
278
|
+
data-id={categoryId}
|
|
279
|
+
>
|
|
280
|
+
<div className="sticky -top-px z-1 bg-popover/90 p-1 py-2 text-sm font-semibold backdrop-blur-xs">
|
|
281
|
+
{i18n.categories[categoryId]}
|
|
282
|
+
</div>
|
|
283
|
+
<div
|
|
284
|
+
className="relative flex flex-wrap"
|
|
285
|
+
style={{ height: section.getRows().length * buttonSize.value }}
|
|
286
|
+
>
|
|
287
|
+
{isCategoryVisible(categoryId) &&
|
|
288
|
+
section
|
|
289
|
+
.getRows()
|
|
290
|
+
.map((row: GridRow) => (
|
|
291
|
+
<RowOfButtons
|
|
292
|
+
key={row.id}
|
|
293
|
+
onMouseOver={onMouseOver}
|
|
294
|
+
onSelectEmoji={onSelectEmoji}
|
|
295
|
+
emojiLibrary={emojiLibrary}
|
|
296
|
+
row={row}
|
|
297
|
+
/>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}),
|
|
303
|
+
[
|
|
304
|
+
emojiLibrary,
|
|
305
|
+
getRowWidth,
|
|
306
|
+
i18n.categories,
|
|
307
|
+
isCategoryVisible,
|
|
308
|
+
onSelectEmoji,
|
|
309
|
+
onMouseOver,
|
|
310
|
+
settings,
|
|
311
|
+
]
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const SearchList = React.useCallback(
|
|
315
|
+
() => (
|
|
316
|
+
<div style={{ width: getRowWidth }} data-id="search">
|
|
317
|
+
<div className="sticky -top-px z-1 bg-popover/90 p-1 py-2 text-sm font-semibold text-card-foreground backdrop-blur-xs">
|
|
318
|
+
{i18n.searchResult}
|
|
319
|
+
</div>
|
|
320
|
+
<div className="relative flex flex-wrap">
|
|
321
|
+
{searchResult.map((emoji: Emoji, index: number) => (
|
|
322
|
+
<EmojiButton
|
|
323
|
+
key={emoji.id}
|
|
324
|
+
onMouseOver={onMouseOver}
|
|
325
|
+
onSelect={onSelectEmoji}
|
|
326
|
+
emoji={emojiLibrary.getEmoji(emoji.id)}
|
|
327
|
+
index={index}
|
|
328
|
+
/>
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
),
|
|
333
|
+
[
|
|
334
|
+
emojiLibrary,
|
|
335
|
+
getRowWidth,
|
|
336
|
+
i18n.searchResult,
|
|
337
|
+
searchResult,
|
|
338
|
+
onSelectEmoji,
|
|
339
|
+
onMouseOver,
|
|
340
|
+
]
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
ref={refs.current.contentRoot}
|
|
346
|
+
className={cn(
|
|
347
|
+
'h-full min-h-[50%] overflow-y-auto overflow-x-hidden px-2',
|
|
348
|
+
'[&::-webkit-scrollbar]:w-4',
|
|
349
|
+
'[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:size-0',
|
|
350
|
+
'[&::-webkit-scrollbar-thumb]:min-h-11 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-thumb]:hover:bg-muted-foreground/25',
|
|
351
|
+
'[&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-popover [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:bg-clip-padding'
|
|
352
|
+
)}
|
|
353
|
+
data-id="scroll"
|
|
354
|
+
>
|
|
355
|
+
<div ref={refs.current.content} className="h-full">
|
|
356
|
+
{isSearching ? SearchList() : EmojiList()}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function EmojiPickerSearchBar({
|
|
363
|
+
children,
|
|
364
|
+
i18n,
|
|
365
|
+
searchValue,
|
|
366
|
+
setSearch,
|
|
367
|
+
}: {
|
|
368
|
+
children: React.ReactNode;
|
|
369
|
+
} & Pick<UseEmojiPickerType, 'i18n' | 'searchValue' | 'setSearch'>) {
|
|
370
|
+
return (
|
|
371
|
+
<div className="flex items-center px-2">
|
|
372
|
+
<div className="relative flex grow items-center">
|
|
373
|
+
<input
|
|
374
|
+
className="block w-full appearance-none rounded-full border-0 bg-muted px-10 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:outline-none"
|
|
375
|
+
value={searchValue}
|
|
376
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
377
|
+
placeholder={i18n.search}
|
|
378
|
+
aria-label="Search"
|
|
379
|
+
autoComplete="off"
|
|
380
|
+
type="text"
|
|
381
|
+
/>
|
|
382
|
+
{children}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function EmojiPickerSearchAndClear({
|
|
389
|
+
clearSearch,
|
|
390
|
+
i18n,
|
|
391
|
+
searchValue,
|
|
392
|
+
}: Pick<UseEmojiPickerType, 'clearSearch' | 'i18n' | 'searchValue'>) {
|
|
393
|
+
return (
|
|
394
|
+
<div className="flex items-center text-foreground">
|
|
395
|
+
<div
|
|
396
|
+
className={cn(
|
|
397
|
+
'absolute left-2.5 top-1/2 z-10 flex size-5 -translate-y-1/2 items-center justify-center text-foreground'
|
|
398
|
+
)}
|
|
399
|
+
>
|
|
400
|
+
{emojiSearchIcons.loupe}
|
|
401
|
+
</div>
|
|
402
|
+
{searchValue && (
|
|
403
|
+
<Button
|
|
404
|
+
size="icon"
|
|
405
|
+
variant="ghost"
|
|
406
|
+
className={cn(
|
|
407
|
+
'absolute right-0.5 top-1/2 flex size-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-popover-foreground hover:bg-transparent'
|
|
408
|
+
)}
|
|
409
|
+
onClick={clearSearch}
|
|
410
|
+
title={i18n.clear}
|
|
411
|
+
aria-label="Clear"
|
|
412
|
+
type="button"
|
|
413
|
+
>
|
|
414
|
+
{emojiSearchIcons.delete}
|
|
415
|
+
</Button>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function EmojiPreview({ emoji }: Pick<UseEmojiPickerType, 'emoji'>) {
|
|
422
|
+
return (
|
|
423
|
+
<div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
|
|
424
|
+
<div className="flex items-center justify-center text-2xl">
|
|
425
|
+
{emoji?.skins[0].native}
|
|
426
|
+
</div>
|
|
427
|
+
<div className="overflow-hidden pl-2">
|
|
428
|
+
<div className="truncate text-sm font-semibold">{emoji?.name}</div>
|
|
429
|
+
<div className="truncate text-sm">{`:${emoji?.id}:`}</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function NoEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
|
|
436
|
+
return (
|
|
437
|
+
<div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
|
|
438
|
+
<div className="flex items-center justify-center text-2xl">😢</div>
|
|
439
|
+
<div className="overflow-hidden pl-2">
|
|
440
|
+
<div className="truncate text-sm font-bold">
|
|
441
|
+
{i18n.searchNoResultsTitle}
|
|
442
|
+
</div>
|
|
443
|
+
<div className="truncate text-sm">{i18n.searchNoResultsSubtitle}</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function PickAnEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
|
|
450
|
+
return (
|
|
451
|
+
<div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
|
|
452
|
+
<div className="flex items-center justify-center text-2xl">☝️</div>
|
|
453
|
+
<div className="overflow-hidden pl-2">
|
|
454
|
+
<div className="truncate text-sm font-semibold">{i18n.pick}</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function EmojiPickerPreview({
|
|
461
|
+
emoji,
|
|
462
|
+
hasFound = true,
|
|
463
|
+
i18n,
|
|
464
|
+
isSearching = false,
|
|
465
|
+
...props
|
|
466
|
+
}: Pick<UseEmojiPickerType, 'emoji' | 'hasFound' | 'i18n' | 'isSearching'>) {
|
|
467
|
+
const showPickEmoji = !emoji && (!isSearching || hasFound);
|
|
468
|
+
const showNoEmoji = isSearching && !hasFound;
|
|
469
|
+
const showPreview = emoji && !showNoEmoji && !showNoEmoji;
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<>
|
|
473
|
+
{showPreview && <EmojiPreview emoji={emoji} {...props} />}
|
|
474
|
+
{showPickEmoji && <PickAnEmoji i18n={i18n} {...props} />}
|
|
475
|
+
{showNoEmoji && <NoEmoji i18n={i18n} {...props} />}
|
|
476
|
+
</>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function EmojiPickerNavigation({
|
|
481
|
+
emojiLibrary,
|
|
482
|
+
focusedCategory,
|
|
483
|
+
i18n,
|
|
484
|
+
icons,
|
|
485
|
+
onClick,
|
|
486
|
+
}: {
|
|
487
|
+
onClick: (id: EmojiCategoryList) => void;
|
|
488
|
+
} & Pick<
|
|
489
|
+
UseEmojiPickerType,
|
|
490
|
+
'emojiLibrary' | 'focusedCategory' | 'i18n' | 'icons'
|
|
491
|
+
>) {
|
|
492
|
+
return (
|
|
493
|
+
<TooltipProvider delayDuration={500}>
|
|
494
|
+
<nav className="mb-2.5 border-b border-b-border border-solid p-1.5">
|
|
495
|
+
<div className="relative flex items-center justify-evenly">
|
|
496
|
+
{emojiLibrary
|
|
497
|
+
.getGrid()
|
|
498
|
+
.sections()
|
|
499
|
+
.map(({ id }: { id: EmojiCategoryList }) => (
|
|
500
|
+
<Tooltip key={id}>
|
|
501
|
+
<TooltipTrigger asChild>
|
|
502
|
+
<Button
|
|
503
|
+
size="sm"
|
|
504
|
+
variant="ghost"
|
|
505
|
+
className={cn(
|
|
506
|
+
'h-fit rounded-full fill-current p-1.5 text-muted-foreground hover:bg-muted hover:text-muted-foreground',
|
|
507
|
+
id === focusedCategory &&
|
|
508
|
+
'pointer-events-none bg-accent fill-current text-accent-foreground'
|
|
509
|
+
)}
|
|
510
|
+
onClick={() => {
|
|
511
|
+
onClick(id);
|
|
512
|
+
}}
|
|
513
|
+
aria-label={i18n.categories[id]}
|
|
514
|
+
type="button"
|
|
515
|
+
>
|
|
516
|
+
<span className="inline-flex size-5 items-center justify-center">
|
|
517
|
+
{icons.categories[id].outline}
|
|
518
|
+
</span>
|
|
519
|
+
</Button>
|
|
520
|
+
</TooltipTrigger>
|
|
521
|
+
<TooltipContent side="bottom">
|
|
522
|
+
{i18n.categories[id]}
|
|
523
|
+
</TooltipContent>
|
|
524
|
+
</Tooltip>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
</nav>
|
|
528
|
+
</TooltipProvider>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const emojiCategoryIcons: Record<
|
|
533
|
+
EmojiCategoryList,
|
|
534
|
+
{
|
|
535
|
+
outline: React.ReactElement;
|
|
536
|
+
solid: React.ReactElement;
|
|
537
|
+
}
|
|
538
|
+
> = {
|
|
539
|
+
activity: {
|
|
540
|
+
outline: (
|
|
541
|
+
<svg
|
|
542
|
+
className="size-full"
|
|
543
|
+
fill="none"
|
|
544
|
+
stroke="currentColor"
|
|
545
|
+
strokeLinecap="round"
|
|
546
|
+
strokeLinejoin="round"
|
|
547
|
+
strokeWidth="2"
|
|
548
|
+
viewBox="0 0 24 24"
|
|
549
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
550
|
+
>
|
|
551
|
+
<title>Activity</title>
|
|
552
|
+
<circle cx="12" cy="12" r="10" />
|
|
553
|
+
<path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
|
|
554
|
+
<path d="m5 4.9 14 14.2" />
|
|
555
|
+
<path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
|
|
556
|
+
</svg>
|
|
557
|
+
),
|
|
558
|
+
solid: (
|
|
559
|
+
<svg
|
|
560
|
+
className="size-full"
|
|
561
|
+
fill="none"
|
|
562
|
+
stroke="currentColor"
|
|
563
|
+
strokeLinecap="round"
|
|
564
|
+
strokeLinejoin="round"
|
|
565
|
+
strokeWidth="2"
|
|
566
|
+
viewBox="0 0 24 24"
|
|
567
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
568
|
+
>
|
|
569
|
+
<title>Activity</title>
|
|
570
|
+
<circle cx="12" cy="12" r="10" />
|
|
571
|
+
<path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
|
|
572
|
+
<path d="m5 4.9 14 14.2" />
|
|
573
|
+
<path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
|
|
574
|
+
</svg>
|
|
575
|
+
),
|
|
576
|
+
},
|
|
577
|
+
custom: {
|
|
578
|
+
outline: <StarIcon className="size-full" />,
|
|
579
|
+
solid: <StarIcon className="size-full" />,
|
|
580
|
+
},
|
|
581
|
+
flags: {
|
|
582
|
+
outline: <FlagIcon className="size-full" />,
|
|
583
|
+
solid: <FlagIcon className="size-full" />,
|
|
584
|
+
},
|
|
585
|
+
foods: {
|
|
586
|
+
outline: <AppleIcon className="size-full" />,
|
|
587
|
+
solid: <AppleIcon className="size-full" />,
|
|
588
|
+
},
|
|
589
|
+
frequent: {
|
|
590
|
+
outline: <ClockIcon className="size-full" />,
|
|
591
|
+
solid: <ClockIcon className="size-full" />,
|
|
592
|
+
},
|
|
593
|
+
nature: {
|
|
594
|
+
outline: <LeafIcon className="size-full" />,
|
|
595
|
+
solid: <LeafIcon className="size-full" />,
|
|
596
|
+
},
|
|
597
|
+
objects: {
|
|
598
|
+
outline: <LightbulbIcon className="size-full" />,
|
|
599
|
+
solid: <LightbulbIcon className="size-full" />,
|
|
600
|
+
},
|
|
601
|
+
people: {
|
|
602
|
+
outline: <SmileIcon className="size-full" />,
|
|
603
|
+
solid: <SmileIcon className="size-full" />,
|
|
604
|
+
},
|
|
605
|
+
places: {
|
|
606
|
+
outline: <CompassIcon className="size-full" />,
|
|
607
|
+
solid: <CompassIcon className="size-full" />,
|
|
608
|
+
},
|
|
609
|
+
symbols: {
|
|
610
|
+
outline: <MusicIcon className="size-full" />,
|
|
611
|
+
solid: <MusicIcon className="size-full" />,
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const emojiSearchIcons = {
|
|
616
|
+
delete: <XIcon className="size-4 text-current" />,
|
|
617
|
+
loupe: <SearchIcon className="size-4 text-current" />,
|
|
618
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type FloatingToolbarState,
|
|
7
|
+
flip,
|
|
8
|
+
offset,
|
|
9
|
+
useFloatingToolbar,
|
|
10
|
+
useFloatingToolbarState,
|
|
11
|
+
} from '@platejs/floating';
|
|
12
|
+
import { useComposedRef } from '@udecode/cn';
|
|
13
|
+
import { KEYS } from 'platejs';
|
|
14
|
+
import {
|
|
15
|
+
useEditorId,
|
|
16
|
+
useEventEditorValue,
|
|
17
|
+
usePluginOption,
|
|
18
|
+
} from 'platejs/react';
|
|
19
|
+
|
|
20
|
+
import { cn } from '@/lib/utils';
|
|
21
|
+
|
|
22
|
+
import { Toolbar } from './toolbar';
|
|
23
|
+
|
|
24
|
+
export function FloatingToolbar({
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
state,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof Toolbar> & {
|
|
30
|
+
state?: FloatingToolbarState;
|
|
31
|
+
}) {
|
|
32
|
+
const editorId = useEditorId();
|
|
33
|
+
const focusedEditorId = useEventEditorValue('focus');
|
|
34
|
+
const isFloatingLinkOpen = !!usePluginOption({ key: KEYS.link }, 'mode');
|
|
35
|
+
|
|
36
|
+
const floatingToolbarState = useFloatingToolbarState({
|
|
37
|
+
editorId,
|
|
38
|
+
focusedEditorId,
|
|
39
|
+
hideToolbar: isFloatingLinkOpen,
|
|
40
|
+
...state,
|
|
41
|
+
floatingOptions: {
|
|
42
|
+
middleware: [
|
|
43
|
+
offset(12),
|
|
44
|
+
flip({
|
|
45
|
+
fallbackPlacements: [
|
|
46
|
+
'top-start',
|
|
47
|
+
'top-end',
|
|
48
|
+
'bottom-start',
|
|
49
|
+
'bottom-end',
|
|
50
|
+
],
|
|
51
|
+
padding: 12,
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
placement: 'top',
|
|
55
|
+
...state?.floatingOptions,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
clickOutsideRef,
|
|
61
|
+
hidden,
|
|
62
|
+
props: rootProps,
|
|
63
|
+
ref: floatingRef,
|
|
64
|
+
} = useFloatingToolbar(floatingToolbarState);
|
|
65
|
+
|
|
66
|
+
const ref = useComposedRef<HTMLDivElement>(props.ref, floatingRef);
|
|
67
|
+
|
|
68
|
+
if (hidden) return null;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={clickOutsideRef}>
|
|
72
|
+
<Toolbar
|
|
73
|
+
{...props}
|
|
74
|
+
{...rootProps}
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn(
|
|
77
|
+
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
|
|
78
|
+
'max-w-[80vw]',
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</Toolbar>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|