@teriusu/rich-editor 0.10.8 → 0.11.0
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/components/BubbleToolbar.svelte +40 -36
- package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/EmojiSuggestionMenu.svelte +291 -0
- package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
- package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
- package/dist/components/FixedToolbar.svelte +53 -47
- package/dist/components/FixedToolbar.svelte.d.ts +3 -0
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FloatingHelper.svelte +219 -0
- package/dist/components/FloatingHelper.svelte.d.ts +14 -0
- package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
- package/dist/components/InputModal.svelte +7 -3
- package/dist/components/InputModal.svelte.d.ts +3 -0
- package/dist/components/InputModal.svelte.d.ts.map +1 -1
- package/dist/components/MathModal.svelte +33 -29
- package/dist/components/MathModal.svelte.d.ts +3 -0
- package/dist/components/MathModal.svelte.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.svelte +7 -3
- package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
- package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +88 -72
- package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TableBubbleMenu.svelte +41 -33
- package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +239 -127
- package/dist/components/TipTapEditor.svelte.d.ts +7 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/CardBlock.d.ts.map +1 -1
- package/dist/extensions/CardBlock.js +6 -4
- package/dist/extensions/FileAttachment.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.js +2 -1
- package/dist/extensions/FixedDetails.d.ts.map +1 -1
- package/dist/extensions/FixedDetails.js +5 -3
- package/dist/extensions/LegacyBlock.d.ts.map +1 -1
- package/dist/extensions/LegacyBlock.js +8 -6
- package/dist/extensions/Math.d.ts.map +1 -1
- package/dist/extensions/Math.js +5 -2
- package/dist/extensions/MbusVideo.d.ts.map +1 -1
- package/dist/extensions/MbusVideo.js +73 -9
- package/dist/extensions/MediaToolbar.d.ts +41 -0
- package/dist/extensions/MediaToolbar.d.ts.map +1 -0
- package/dist/extensions/MediaToolbar.js +196 -0
- package/dist/extensions/MediaToolbar.test.d.ts +2 -0
- package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
- package/dist/extensions/MediaToolbar.test.js +231 -0
- package/dist/extensions/OrderedListMarker.d.ts +2 -0
- package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
- package/dist/extensions/OrderedListMarker.js +114 -0
- package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
- package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
- package/dist/extensions/OrderedListMarker.test.js +176 -0
- package/dist/extensions/PdfBlock.d.ts.map +1 -1
- package/dist/extensions/PdfBlock.js +25 -23
- package/dist/extensions/ResizableImage.d.ts.map +1 -1
- package/dist/extensions/ResizableImage.js +20 -1
- package/dist/extensions/TableKeymap.d.ts +16 -0
- package/dist/extensions/TableKeymap.d.ts.map +1 -0
- package/dist/extensions/TableKeymap.js +97 -0
- package/dist/extensions/TableKeymap.test.d.ts +2 -0
- package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
- package/dist/extensions/TableKeymap.test.js +147 -0
- package/dist/extensions/TabsBlock.d.ts.map +1 -1
- package/dist/extensions/TabsBlock.js +12 -9
- package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
- package/dist/extensions/TiptapMidibus.js +60 -6
- package/dist/extensions/UploadSkeleton.d.ts +61 -0
- package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
- package/dist/extensions/UploadSkeleton.js +233 -0
- package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
- package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
- package/dist/extensions/UploadSkeleton.test.js +128 -0
- package/dist/extensions/VideoEmbed.d.ts +8 -0
- package/dist/extensions/VideoEmbed.d.ts.map +1 -1
- package/dist/extensions/VideoEmbed.js +226 -45
- package/dist/extensions/VideoEmbed.test.js +131 -2
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +191 -0
- package/dist/i18n/es.d.ts +3 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +191 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/index.test.d.ts +2 -0
- package/dist/i18n/index.test.d.ts.map +1 -0
- package/dist/i18n/index.test.js +173 -0
- package/dist/i18n/ja.d.ts +3 -0
- package/dist/i18n/ja.d.ts.map +1 -0
- package/dist/i18n/ja.js +191 -0
- package/dist/i18n/ko.d.ts +198 -0
- package/dist/i18n/ko.d.ts.map +1 -0
- package/dist/i18n/ko.js +215 -0
- package/dist/i18n/zh-hans.d.ts +3 -0
- package/dist/i18n/zh-hans.d.ts.map +1 -0
- package/dist/i18n/zh-hans.js +191 -0
- package/dist/i18n/zh-hant.d.ts +3 -0
- package/dist/i18n/zh-hant.d.ts.map +1 -0
- package/dist/i18n/zh-hant.js +192 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/static/highlight.d.ts +13 -0
- package/dist/static/highlight.d.ts.map +1 -0
- package/dist/static/highlight.js +99 -0
- package/dist/static/index.d.ts +33 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +367 -0
- package/dist/static/index.test.d.ts +2 -0
- package/dist/static/index.test.d.ts.map +1 -0
- package/dist/static/index.test.js +194 -0
- package/dist/static/tokenizer.d.ts +52 -0
- package/dist/static/tokenizer.d.ts.map +1 -0
- package/dist/static/tokenizer.js +289 -0
- package/dist/styles/editor.css +1 -1
- package/dist/utils/emoji-data.d.ts +15 -0
- package/dist/utils/emoji-suggestion.d.ts +48 -0
- package/dist/utils/emoji-suggestion.d.ts.map +1 -0
- package/dist/utils/emoji-suggestion.js +51 -0
- package/dist/utils/emoji-suggestion.test.d.ts +2 -0
- package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
- package/dist/utils/emoji-suggestion.test.js +117 -0
- package/dist/utils/floating-helper.d.ts +60 -0
- package/dist/utils/floating-helper.d.ts.map +1 -0
- package/dist/utils/floating-helper.js +63 -0
- package/dist/utils/floating-helper.test.d.ts +2 -0
- package/dist/utils/floating-helper.test.d.ts.map +1 -0
- package/dist/utils/floating-helper.test.js +141 -0
- package/dist/utils/guard-vscode-paste.d.ts +19 -0
- package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
- package/dist/utils/guard-vscode-paste.js +46 -0
- package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
- package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
- package/dist/utils/guard-vscode-paste.test.js +86 -0
- package/dist/utils/media-size.d.ts +29 -0
- package/dist/utils/media-size.d.ts.map +1 -0
- package/dist/utils/media-size.js +90 -0
- package/dist/utils/resize.d.ts +12 -0
- package/dist/utils/resize.d.ts.map +1 -1
- package/dist/utils/resize.js +5 -5
- package/dist/utils/sanitize.d.ts +2 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +28 -3
- package/package.json +8 -1
|
@@ -28,15 +28,19 @@
|
|
|
28
28
|
} from "lucide-svelte";
|
|
29
29
|
import { cn } from "../utils/cn";
|
|
30
30
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
31
|
+
import { defaultTranslator, type EditorMessageKey, type EditorTranslator } from "../i18n";
|
|
31
32
|
|
|
32
33
|
let {
|
|
33
34
|
editor,
|
|
34
35
|
features,
|
|
35
36
|
onPromptLink,
|
|
37
|
+
t = defaultTranslator,
|
|
36
38
|
}: {
|
|
37
39
|
editor: Editor;
|
|
38
40
|
features: Set<ToolbarFeature>;
|
|
39
41
|
onPromptLink?: PromptHandler;
|
|
42
|
+
/** 에디터 UI 번역 함수. 미주입 시 ko. */
|
|
43
|
+
t?: EditorTranslator;
|
|
40
44
|
} = $props();
|
|
41
45
|
|
|
42
46
|
/*
|
|
@@ -72,27 +76,27 @@
|
|
|
72
76
|
let colorMenuEl: HTMLDivElement | undefined = $state();
|
|
73
77
|
const iconSize = 14;
|
|
74
78
|
|
|
75
|
-
const TEXT_COLORS = [
|
|
76
|
-
{
|
|
77
|
-
{
|
|
78
|
-
{
|
|
79
|
-
{
|
|
80
|
-
{
|
|
81
|
-
{
|
|
82
|
-
{
|
|
83
|
-
{
|
|
84
|
-
{
|
|
79
|
+
const TEXT_COLORS: { labelKey: EditorMessageKey; value: string }[] = [
|
|
80
|
+
{ labelKey: "colorDefault", value: "" },
|
|
81
|
+
{ labelKey: "colorBlack", value: "#000000" },
|
|
82
|
+
{ labelKey: "colorGray", value: "#6b7280" },
|
|
83
|
+
{ labelKey: "colorRed", value: "#dc2626" },
|
|
84
|
+
{ labelKey: "colorOrange", value: "#ea580c" },
|
|
85
|
+
{ labelKey: "colorYellow", value: "#ca8a04" },
|
|
86
|
+
{ labelKey: "colorGreen", value: "#16a34a" },
|
|
87
|
+
{ labelKey: "colorBlue", value: "#2563eb" },
|
|
88
|
+
{ labelKey: "colorPurple", value: "#7c3aed" }
|
|
85
89
|
];
|
|
86
90
|
|
|
87
91
|
function getCurrentBlockLabel(): string {
|
|
88
|
-
if (isActive("heading", { level: 1 })) return "
|
|
89
|
-
if (isActive("heading", { level: 2 })) return "
|
|
90
|
-
if (isActive("heading", { level: 3 })) return "
|
|
91
|
-
if (isActive("bulletList")) return "
|
|
92
|
-
if (isActive("orderedList")) return "
|
|
93
|
-
if (isActive("taskList")) return "
|
|
94
|
-
if (isActive("blockquote")) return "
|
|
95
|
-
return "
|
|
92
|
+
if (isActive("heading", { level: 1 })) return t("heading1");
|
|
93
|
+
if (isActive("heading", { level: 2 })) return t("heading2");
|
|
94
|
+
if (isActive("heading", { level: 3 })) return t("heading3");
|
|
95
|
+
if (isActive("bulletList")) return t("bulletList");
|
|
96
|
+
if (isActive("orderedList")) return t("orderedList");
|
|
97
|
+
if (isActive("taskList")) return t("taskList");
|
|
98
|
+
if (isActive("blockquote")) return t("blockquote");
|
|
99
|
+
return t("paragraph");
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
function isParagraphActive(): boolean {
|
|
@@ -109,7 +113,7 @@
|
|
|
109
113
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
110
114
|
const url = onPromptLink
|
|
111
115
|
? await onPromptLink(previousUrl)
|
|
112
|
-
: window.prompt("
|
|
116
|
+
: window.prompt(t("promptLinkUrl"), previousUrl);
|
|
113
117
|
if (url === null) return;
|
|
114
118
|
if (url === "") {
|
|
115
119
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
@@ -265,7 +269,7 @@
|
|
|
265
269
|
showBlockMenu = false;
|
|
266
270
|
}}
|
|
267
271
|
>
|
|
268
|
-
<Type size={12} />
|
|
272
|
+
<Type size={12} /> {t('paragraph')}
|
|
269
273
|
</button>
|
|
270
274
|
{#each [1, 2, 3] as level}
|
|
271
275
|
{#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
|
|
@@ -287,7 +291,7 @@
|
|
|
287
291
|
showBlockMenu = false;
|
|
288
292
|
}}
|
|
289
293
|
>
|
|
290
|
-
<Icon size={12} />
|
|
294
|
+
<Icon size={12} /> {t(level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3')}
|
|
291
295
|
</button>
|
|
292
296
|
{/if}
|
|
293
297
|
{/each}
|
|
@@ -305,7 +309,7 @@
|
|
|
305
309
|
showBlockMenu = false;
|
|
306
310
|
}}
|
|
307
311
|
>
|
|
308
|
-
<List size={12} />
|
|
312
|
+
<List size={12} /> {t('bulletList')}
|
|
309
313
|
</button>
|
|
310
314
|
{/if}
|
|
311
315
|
{#if has('ordered-list')}
|
|
@@ -322,7 +326,7 @@
|
|
|
322
326
|
showBlockMenu = false;
|
|
323
327
|
}}
|
|
324
328
|
>
|
|
325
|
-
<ListOrdered size={12} />
|
|
329
|
+
<ListOrdered size={12} /> {t('orderedList')}
|
|
326
330
|
</button>
|
|
327
331
|
{/if}
|
|
328
332
|
{#if has('checklist')}
|
|
@@ -339,7 +343,7 @@
|
|
|
339
343
|
showBlockMenu = false;
|
|
340
344
|
}}
|
|
341
345
|
>
|
|
342
|
-
<ListChecks size={12} />
|
|
346
|
+
<ListChecks size={12} /> {t('taskList')}
|
|
343
347
|
</button>
|
|
344
348
|
{/if}
|
|
345
349
|
{#if has('blockquote')}
|
|
@@ -356,7 +360,7 @@
|
|
|
356
360
|
showBlockMenu = false;
|
|
357
361
|
}}
|
|
358
362
|
>
|
|
359
|
-
<Quote size={12} />
|
|
363
|
+
<Quote size={12} /> {t('blockquote')}
|
|
360
364
|
</button>
|
|
361
365
|
{/if}
|
|
362
366
|
</div>
|
|
@@ -371,7 +375,7 @@
|
|
|
371
375
|
<button
|
|
372
376
|
type="button"
|
|
373
377
|
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
374
|
-
aria-label=
|
|
378
|
+
aria-label={t('bold')}
|
|
375
379
|
class={cn(
|
|
376
380
|
"p-1.5 rounded-full transition-colors",
|
|
377
381
|
isActive("bold")
|
|
@@ -386,7 +390,7 @@
|
|
|
386
390
|
<button
|
|
387
391
|
type="button"
|
|
388
392
|
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
389
|
-
aria-label=
|
|
393
|
+
aria-label={t('italic')}
|
|
390
394
|
class={cn(
|
|
391
395
|
"p-1.5 rounded-full transition-colors",
|
|
392
396
|
isActive("italic")
|
|
@@ -401,7 +405,7 @@
|
|
|
401
405
|
<button
|
|
402
406
|
type="button"
|
|
403
407
|
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
404
|
-
aria-label=
|
|
408
|
+
aria-label={t('underline')}
|
|
405
409
|
class={cn(
|
|
406
410
|
"p-1.5 rounded-full transition-colors",
|
|
407
411
|
isActive("underline")
|
|
@@ -416,7 +420,7 @@
|
|
|
416
420
|
<button
|
|
417
421
|
type="button"
|
|
418
422
|
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
419
|
-
aria-label=
|
|
423
|
+
aria-label={t('strike')}
|
|
420
424
|
class={cn(
|
|
421
425
|
"p-1.5 rounded-full transition-colors",
|
|
422
426
|
isActive("strike")
|
|
@@ -433,7 +437,7 @@
|
|
|
433
437
|
<button
|
|
434
438
|
type="button"
|
|
435
439
|
onclick={() => editor.chain().focus().toggleCode().run()}
|
|
436
|
-
aria-label=
|
|
440
|
+
aria-label={t('code')}
|
|
437
441
|
class={cn(
|
|
438
442
|
"p-1.5 rounded-full transition-colors",
|
|
439
443
|
isActive("code")
|
|
@@ -454,7 +458,7 @@
|
|
|
454
458
|
<button
|
|
455
459
|
type="button"
|
|
456
460
|
onclick={() => editor.chain().focus().toggleMathInline().run()}
|
|
457
|
-
aria-label=
|
|
461
|
+
aria-label={t('mathInline')}
|
|
458
462
|
class={cn(
|
|
459
463
|
"p-1.5 rounded-full transition-colors",
|
|
460
464
|
isActive("math_inline")
|
|
@@ -474,7 +478,7 @@
|
|
|
474
478
|
<button
|
|
475
479
|
type="button"
|
|
476
480
|
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
477
|
-
aria-label=
|
|
481
|
+
aria-label={t('highlight')}
|
|
478
482
|
class={cn(
|
|
479
483
|
"p-1.5 rounded-full transition-colors",
|
|
480
484
|
isActive("highlight")
|
|
@@ -490,7 +494,7 @@
|
|
|
490
494
|
<button
|
|
491
495
|
type="button"
|
|
492
496
|
onclick={() => (showColors = !showColors)}
|
|
493
|
-
aria-label=
|
|
497
|
+
aria-label={t('textColor')}
|
|
494
498
|
class={cn(
|
|
495
499
|
"p-1.5 rounded-full transition-colors",
|
|
496
500
|
editor.getAttributes("textStyle").color
|
|
@@ -512,7 +516,7 @@
|
|
|
512
516
|
{#each TEXT_COLORS as c}
|
|
513
517
|
<button
|
|
514
518
|
type="button"
|
|
515
|
-
title={c.
|
|
519
|
+
title={t(c.labelKey)}
|
|
516
520
|
class="h-7 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold"
|
|
517
521
|
style="color: {c.value || '#000'}; background: #fff"
|
|
518
522
|
onclick={() => {
|
|
@@ -531,7 +535,7 @@
|
|
|
531
535
|
<label
|
|
532
536
|
class="hce-color-divider mt-2 pt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
|
|
533
537
|
>
|
|
534
|
-
<span
|
|
538
|
+
<span>{t('customColor')}</span>
|
|
535
539
|
<input
|
|
536
540
|
type="color"
|
|
537
541
|
class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
|
|
@@ -553,7 +557,7 @@
|
|
|
553
557
|
<button
|
|
554
558
|
type="button"
|
|
555
559
|
onclick={addLink}
|
|
556
|
-
aria-label=
|
|
560
|
+
aria-label={t('link')}
|
|
557
561
|
class={cn(
|
|
558
562
|
"p-1.5 rounded-full transition-colors",
|
|
559
563
|
isActive("link")
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
2
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
3
|
+
import { type EditorTranslator } from "../i18n";
|
|
3
4
|
type $$ComponentProps = {
|
|
4
5
|
editor: Editor;
|
|
5
6
|
features: Set<ToolbarFeature>;
|
|
6
7
|
onPromptLink?: PromptHandler;
|
|
8
|
+
/** 에디터 UI 번역 함수. 미주입 시 ko. */
|
|
9
|
+
t?: EditorTranslator;
|
|
7
10
|
};
|
|
8
11
|
declare const BubbleToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
12
|
type BubbleToolbar = ReturnType<typeof BubbleToolbar>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AAgcJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import {
|
|
4
|
+
findEmojiQuery,
|
|
5
|
+
searchEmojis,
|
|
6
|
+
type EmojiSuggestion,
|
|
7
|
+
} from "../utils/emoji-suggestion";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* 이모지 `:` 자동완성의 **화면 절반** — 판정·검색은 `utils/emoji-suggestion.ts`.
|
|
11
|
+
*
|
|
12
|
+
* 슬래시 메뉴와 달리 상태(열림·위치·검색어)를 이 컴포넌트가 전부 들고 있다.
|
|
13
|
+
* `TipTapEditor` 는 마운트만 한다 — 에디터 update/selectionUpdate 를 여기서 직접
|
|
14
|
+
* 구독하므로 에디터 쪽에 끼워 넣을 배선이 없다.
|
|
15
|
+
*
|
|
16
|
+
* 키보드는 슬래시 메뉴(`SlashCommandMenu`)와 같은 방식: 문서 캡처 단계에서 가로챈다.
|
|
17
|
+
* `preventDefault` 만으로 충분하다 — prosemirror-view 는 `defaultPrevented` 인 keydown
|
|
18
|
+
* 을 버린다(설치본 `input.ts` 확인). Tab 이동은 main(emoji.ts)에서 가져온 것이다.
|
|
19
|
+
*/
|
|
20
|
+
/*
|
|
21
|
+
* `onOpenChange` 는 실표시(`visible`) 기준 알림이다 — `TipTapEditor` 가 빈 줄 플로팅
|
|
22
|
+
* 헬퍼를 이 팝업과 동시에 띄우지 않으려고 받는다. 열려 있어도 후보가 없어 안 그리면
|
|
23
|
+
* (`visible` 주석) 닫힌 것으로 알린다.
|
|
24
|
+
*/
|
|
25
|
+
let {
|
|
26
|
+
editor,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
}: { editor: Editor; onOpenChange?: (open: boolean) => void } = $props();
|
|
29
|
+
|
|
30
|
+
let open = $state(false);
|
|
31
|
+
let pos = $state({ top: 0, left: 0 });
|
|
32
|
+
let query = $state("");
|
|
33
|
+
let range: { from: number; to: number } | null = null;
|
|
34
|
+
let selectedIndex = $state(0);
|
|
35
|
+
let menuEl: HTMLDivElement | undefined = $state();
|
|
36
|
+
let mouseMovedSinceKeyboard = $state(true);
|
|
37
|
+
|
|
38
|
+
const items = $derived(searchEmojis(query));
|
|
39
|
+
/*
|
|
40
|
+
* 열려 있어도 후보가 없으면 **없는 것처럼** 군다 — 빈 상자를 띄우지 않고, 키보드도
|
|
41
|
+
* 가로채지 않는다(`:x` 뒤에 일반 문장을 이어 칠 때 Enter 를 뺏으면 안 된다).
|
|
42
|
+
*/
|
|
43
|
+
const visible = $derived(open && items.length > 0);
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
onOpenChange?.(visible);
|
|
47
|
+
/* 열린 채로 내려가는 경우(editable 전환 등)에도 닫힘을 알린다. */
|
|
48
|
+
return () => {
|
|
49
|
+
if (visible) onOpenChange?.(false);
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const MENU_HEIGHT = 320;
|
|
54
|
+
|
|
55
|
+
/* `updateSlashMenuPosition`(TipTapEditor)과 같은 계산 — 고정 툴바·하단 바를 피해 뒤집는다. */
|
|
56
|
+
function updatePosition() {
|
|
57
|
+
if (!range) return;
|
|
58
|
+
const coords = editor.view.coordsAtPos(range.to);
|
|
59
|
+
|
|
60
|
+
const headerHeight = parseInt(
|
|
61
|
+
getComputedStyle(document.documentElement).getPropertyValue(
|
|
62
|
+
"--header-height",
|
|
63
|
+
) || "74",
|
|
64
|
+
);
|
|
65
|
+
const toolbarEl = editor.view.dom
|
|
66
|
+
.closest(".hce-editor-wrapper")
|
|
67
|
+
?.querySelector(".sticky");
|
|
68
|
+
const toolbarBottom = toolbarEl
|
|
69
|
+
? toolbarEl.getBoundingClientRect().bottom
|
|
70
|
+
: headerHeight;
|
|
71
|
+
const topSafe = Math.max(toolbarBottom, headerHeight) + 4;
|
|
72
|
+
|
|
73
|
+
const bottomBar = document.querySelector(".sticky.bottom-0");
|
|
74
|
+
const bottomBarHeight = bottomBar
|
|
75
|
+
? bottomBar.getBoundingClientRect().height
|
|
76
|
+
: 0;
|
|
77
|
+
const bottomSafe = window.innerHeight - bottomBarHeight - 4;
|
|
78
|
+
|
|
79
|
+
const spaceBelow = bottomSafe - coords.bottom;
|
|
80
|
+
const spaceAbove = coords.top - topSafe;
|
|
81
|
+
const goUp = spaceBelow < MENU_HEIGHT && spaceAbove > spaceBelow;
|
|
82
|
+
|
|
83
|
+
let top: number;
|
|
84
|
+
if (goUp) {
|
|
85
|
+
top = Math.max(topSafe, coords.top - MENU_HEIGHT);
|
|
86
|
+
} else {
|
|
87
|
+
top = Math.min(coords.bottom + 4, bottomSafe - MENU_HEIGHT);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pos = { top, left: coords.left };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function close() {
|
|
94
|
+
open = false;
|
|
95
|
+
range = null;
|
|
96
|
+
query = "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* 문서가 바뀔 때(타이핑) — 열고 닫기 전부 판정한다. */
|
|
100
|
+
function handleUpdate() {
|
|
101
|
+
const match = findEmojiQuery(editor.state);
|
|
102
|
+
if (!match) {
|
|
103
|
+
if (open) close();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
range = { from: match.from, to: match.to };
|
|
107
|
+
if (match.query !== query) {
|
|
108
|
+
query = match.query;
|
|
109
|
+
selectedIndex = 0;
|
|
110
|
+
}
|
|
111
|
+
open = true;
|
|
112
|
+
updatePosition();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* 선택만 움직일 때(클릭·화살표 이동)는 **닫기만** 판정한다 — 슬래시 메뉴의
|
|
117
|
+
* `handleSelectionUpdate` 와 같은 비대칭. 옛 `:word` 텍스트를 클릭했다고 팝업이
|
|
118
|
+
* 열리면 안 된다.
|
|
119
|
+
*/
|
|
120
|
+
function handleSelectionUpdate() {
|
|
121
|
+
if (!open) return;
|
|
122
|
+
if (!findEmojiQuery(editor.state)) close();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
editor.on("update", handleUpdate);
|
|
127
|
+
editor.on("selectionUpdate", handleSelectionUpdate);
|
|
128
|
+
return () => {
|
|
129
|
+
editor.off("update", handleUpdate);
|
|
130
|
+
editor.off("selectionUpdate", handleSelectionUpdate);
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function select(item: EmojiSuggestion) {
|
|
135
|
+
if (!range) return;
|
|
136
|
+
const { from, to } = range;
|
|
137
|
+
close();
|
|
138
|
+
// main 과 같은 삽입 꼴 — `:query` 를 지우고 이모지 + 공백.
|
|
139
|
+
editor
|
|
140
|
+
.chain()
|
|
141
|
+
.focus()
|
|
142
|
+
.deleteRange({ from, to })
|
|
143
|
+
.insertContent(`${item.emoji} `)
|
|
144
|
+
.run();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$effect(() => {
|
|
148
|
+
if (!visible) return;
|
|
149
|
+
const onScroll = () => updatePosition();
|
|
150
|
+
window.addEventListener("scroll", onScroll, true);
|
|
151
|
+
return () => window.removeEventListener("scroll", onScroll, true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
$effect(() => {
|
|
155
|
+
if (!visible) return;
|
|
156
|
+
function handleMouseDown(e: MouseEvent) {
|
|
157
|
+
if (menuEl && !menuEl.contains(e.target as Node)) close();
|
|
158
|
+
}
|
|
159
|
+
document.addEventListener("mousedown", handleMouseDown, true);
|
|
160
|
+
return () =>
|
|
161
|
+
document.removeEventListener("mousedown", handleMouseDown, true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
$effect(() => {
|
|
165
|
+
if (!visible) return;
|
|
166
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
167
|
+
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
mouseMovedSinceKeyboard = false;
|
|
170
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
171
|
+
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
mouseMovedSinceKeyboard = false;
|
|
174
|
+
selectedIndex =
|
|
175
|
+
selectedIndex <= 0 ? items.length - 1 : selectedIndex - 1;
|
|
176
|
+
} else if (e.key === "Enter") {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
const item = items[selectedIndex];
|
|
179
|
+
if (item) select(item);
|
|
180
|
+
} else if (e.key === "Escape") {
|
|
181
|
+
// main 처럼 친 텍스트는 남기고 팝업만 접는다(다음 글자를 치면 다시 뜬다).
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
open = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
187
|
+
return () => document.removeEventListener("keydown", handleKeyDown, true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
$effect(() => {
|
|
191
|
+
// 선택 항목을 보이는 자리로
|
|
192
|
+
if (menuEl) {
|
|
193
|
+
const el = menuEl.querySelector(`[data-index="${selectedIndex}"]`);
|
|
194
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
</script>
|
|
198
|
+
|
|
199
|
+
{#if visible}
|
|
200
|
+
<div class="fixed z-50" style="top: {pos.top}px; left: {pos.left}px">
|
|
201
|
+
<div
|
|
202
|
+
bind:this={menuEl}
|
|
203
|
+
class="emoji-menu z-50 bg-popover border border-border rounded-xl shadow-xl overflow-y-auto py-1.5"
|
|
204
|
+
>
|
|
205
|
+
{#each items as item, i (item.keyword)}
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
data-index={i}
|
|
209
|
+
class="emoji-item {i === selectedIndex ? 'is-selected' : ''}"
|
|
210
|
+
onmousemove={() => {
|
|
211
|
+
if (!mouseMovedSinceKeyboard) {
|
|
212
|
+
mouseMovedSinceKeyboard = true;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
selectedIndex = i;
|
|
216
|
+
}}
|
|
217
|
+
onclick={() => select(item)}
|
|
218
|
+
>
|
|
219
|
+
<span class="emoji-glyph">{item.emoji}</span>
|
|
220
|
+
<span class="emoji-keyword">{item.keyword}</span>
|
|
221
|
+
</button>
|
|
222
|
+
{/each}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
{/if}
|
|
226
|
+
|
|
227
|
+
<style>
|
|
228
|
+
.emoji-menu {
|
|
229
|
+
width: 224px;
|
|
230
|
+
max-height: 320px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* `SlashCommandMenu` 의 .slash-item 과 같은 결 — 두 팝업이 같은 물건으로 보여야 한다. */
|
|
234
|
+
.emoji-item {
|
|
235
|
+
position: relative;
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 8px;
|
|
239
|
+
width: 100%;
|
|
240
|
+
padding: 4px 10px;
|
|
241
|
+
text-align: left;
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
font-weight: 500;
|
|
244
|
+
color: var(--foreground);
|
|
245
|
+
background: transparent;
|
|
246
|
+
border: 0;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
transition: background-color 0.12s;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.emoji-item:hover,
|
|
252
|
+
.emoji-item.is-selected {
|
|
253
|
+
background: color-mix(in srgb, var(--primary) 8%, transparent);
|
|
254
|
+
color: var(--primary);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.emoji-item.is-selected::before {
|
|
258
|
+
content: '';
|
|
259
|
+
position: absolute;
|
|
260
|
+
left: 0;
|
|
261
|
+
top: 3px;
|
|
262
|
+
bottom: 3px;
|
|
263
|
+
width: 3px;
|
|
264
|
+
border-radius: 0 2px 2px 0;
|
|
265
|
+
background: var(--primary);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.emoji-glyph {
|
|
269
|
+
flex-shrink: 0;
|
|
270
|
+
width: 20px;
|
|
271
|
+
font-size: 15px;
|
|
272
|
+
line-height: 1;
|
|
273
|
+
text-align: center;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.emoji-keyword {
|
|
277
|
+
flex: 1;
|
|
278
|
+
min-width: 0;
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
text-overflow: ellipsis;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
283
|
+
font-size: 11px;
|
|
284
|
+
color: var(--muted-foreground);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.emoji-item.is-selected .emoji-keyword,
|
|
288
|
+
.emoji-item:hover .emoji-keyword {
|
|
289
|
+
color: var(--primary);
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
onOpenChange?: (open: boolean) => void;
|
|
5
|
+
};
|
|
6
|
+
declare const EmojiSuggestionMenu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type EmojiSuggestionMenu = ReturnType<typeof EmojiSuggestionMenu>;
|
|
8
|
+
export default EmojiSuggestionMenu;
|
|
9
|
+
//# sourceMappingURL=EmojiSuggestionMenu.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmojiSuggestionMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/EmojiSuggestionMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAO1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CAAC;AA0NrF,QAAA,MAAM,mBAAmB,sDAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|