@teriusu/rich-editor 0.10.8 → 0.11.1
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 +182 -54
- 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
|
@@ -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"}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import ToggleHeadingIcon from "./icons/ToggleHeadingIcon.svelte";
|
|
42
42
|
import InputModal from "./InputModal.svelte";
|
|
43
43
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
44
|
+
import { defaultTranslator, type EditorTranslator } from "../i18n";
|
|
44
45
|
|
|
45
46
|
let {
|
|
46
47
|
editor,
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
onPromptMbus,
|
|
53
54
|
onPromptVideo,
|
|
54
55
|
toolbarEnd,
|
|
56
|
+
t = defaultTranslator,
|
|
55
57
|
}: {
|
|
56
58
|
editor: Editor;
|
|
57
59
|
features: Set<ToolbarFeature>;
|
|
@@ -76,6 +78,8 @@
|
|
|
76
78
|
* 여기에 넣으면 툴바의 flex 행에 그대로 얹혀 정렬이 저절로 맞는다.
|
|
77
79
|
*/
|
|
78
80
|
toolbarEnd?: Snippet;
|
|
81
|
+
/** 에디터 UI 번역 함수. 미주입 시 ko. */
|
|
82
|
+
t?: EditorTranslator;
|
|
79
83
|
} = $props();
|
|
80
84
|
|
|
81
85
|
/*
|
|
@@ -171,21 +175,21 @@
|
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
const currentBlockLabel = $derived.by(() => {
|
|
174
|
-
if (isActive("heading", { level: 1 })) return "
|
|
175
|
-
if (isActive("heading", { level: 2 })) return "
|
|
176
|
-
if (isActive("heading", { level: 3 })) return "
|
|
177
|
-
if (isActive("bulletList")) return "
|
|
178
|
-
if (isActive("orderedList")) return "
|
|
179
|
-
if (isActive("taskList")) return "
|
|
180
|
-
if (isActive("blockquote")) return "
|
|
178
|
+
if (isActive("heading", { level: 1 })) return t("heading1");
|
|
179
|
+
if (isActive("heading", { level: 2 })) return t("heading2");
|
|
180
|
+
if (isActive("heading", { level: 3 })) return t("heading3");
|
|
181
|
+
if (isActive("bulletList")) return t("bulletList");
|
|
182
|
+
if (isActive("orderedList")) return t("orderedList");
|
|
183
|
+
if (isActive("taskList")) return t("taskList");
|
|
184
|
+
if (isActive("blockquote")) return t("blockquote");
|
|
181
185
|
if (isActive("details")) {
|
|
182
186
|
// 토글 제목이면 단계까지 보여 준다 — 그냥 "토글" 이면 무엇을 고른 건지 안 보인다.
|
|
183
187
|
for (const lv of [1, 2, 3]) {
|
|
184
|
-
if (isActive("detailsSummary", { level: lv })) return
|
|
188
|
+
if (isActive("detailsSummary", { level: lv })) return t("toggleHeading", { level: lv });
|
|
185
189
|
}
|
|
186
|
-
return "
|
|
190
|
+
return t("toggle");
|
|
187
191
|
}
|
|
188
|
-
return "
|
|
192
|
+
return t("paragraph");
|
|
189
193
|
});
|
|
190
194
|
|
|
191
195
|
function runBlock(fn: () => void) {
|
|
@@ -238,7 +242,7 @@
|
|
|
238
242
|
type="button"
|
|
239
243
|
onclick={() => editor.chain().focus().undo().run()}
|
|
240
244
|
disabled={!canDo((c) => c.undo())}
|
|
241
|
-
aria-label=
|
|
245
|
+
aria-label={t('undo')}
|
|
242
246
|
class={cn(
|
|
243
247
|
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
244
248
|
!canDo((c) => c.undo()) && "opacity-30 pointer-events-none",
|
|
@@ -252,7 +256,7 @@
|
|
|
252
256
|
type="button"
|
|
253
257
|
onclick={() => editor.chain().focus().redo().run()}
|
|
254
258
|
disabled={!canDo((c) => c.redo())}
|
|
255
|
-
aria-label=
|
|
259
|
+
aria-label={t('redo')}
|
|
256
260
|
class={cn(
|
|
257
261
|
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
258
262
|
!canDo((c) => c.redo()) && "opacity-30 pointer-events-none",
|
|
@@ -271,7 +275,7 @@
|
|
|
271
275
|
<button
|
|
272
276
|
type="button"
|
|
273
277
|
onclick={() => (blockMenuOpen = !blockMenuOpen)}
|
|
274
|
-
aria-label=
|
|
278
|
+
aria-label={t('blockType')}
|
|
275
279
|
class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground min-w-[96px]"
|
|
276
280
|
>
|
|
277
281
|
<span class="text-sm">{currentBlockLabel}</span>
|
|
@@ -301,7 +305,7 @@
|
|
|
301
305
|
onclick={() =>
|
|
302
306
|
runBlock(() => editor.chain().focus().setParagraph().run())}
|
|
303
307
|
>
|
|
304
|
-
<Pilcrow size={14} />
|
|
308
|
+
<Pilcrow size={14} /> {t('paragraph')}
|
|
305
309
|
</button>
|
|
306
310
|
{#if has('h1')}
|
|
307
311
|
<button
|
|
@@ -315,7 +319,7 @@
|
|
|
315
319
|
editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
316
320
|
)}
|
|
317
321
|
>
|
|
318
|
-
<Heading1 size={14} />
|
|
322
|
+
<Heading1 size={14} /> {t('heading1')}
|
|
319
323
|
<span class="hce-menu-shortcut"># </span>
|
|
320
324
|
</button>
|
|
321
325
|
{/if}
|
|
@@ -331,7 +335,7 @@
|
|
|
331
335
|
editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
332
336
|
)}
|
|
333
337
|
>
|
|
334
|
-
<Heading2 size={14} />
|
|
338
|
+
<Heading2 size={14} /> {t('heading2')}
|
|
335
339
|
<span class="hce-menu-shortcut">## </span>
|
|
336
340
|
</button>
|
|
337
341
|
{/if}
|
|
@@ -347,7 +351,7 @@
|
|
|
347
351
|
editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
348
352
|
)}
|
|
349
353
|
>
|
|
350
|
-
<Heading3 size={14} />
|
|
354
|
+
<Heading3 size={14} /> {t('heading3')}
|
|
351
355
|
<span class="hce-menu-shortcut">### </span>
|
|
352
356
|
</button>
|
|
353
357
|
{/if}
|
|
@@ -364,7 +368,7 @@
|
|
|
364
368
|
onclick={() =>
|
|
365
369
|
runBlock(() => editor.chain().focus().toggleBulletList().run())}
|
|
366
370
|
>
|
|
367
|
-
<List size={14} />
|
|
371
|
+
<List size={14} /> {t('bulletList')}
|
|
368
372
|
<span class="hce-menu-shortcut">- </span>
|
|
369
373
|
</button>
|
|
370
374
|
{/if}
|
|
@@ -378,7 +382,7 @@
|
|
|
378
382
|
onclick={() =>
|
|
379
383
|
runBlock(() => editor.chain().focus().toggleOrderedList().run())}
|
|
380
384
|
>
|
|
381
|
-
<ListOrdered size={14} />
|
|
385
|
+
<ListOrdered size={14} /> {t('orderedList')}
|
|
382
386
|
<span class="hce-menu-shortcut">1. </span>
|
|
383
387
|
</button>
|
|
384
388
|
{/if}
|
|
@@ -392,7 +396,7 @@
|
|
|
392
396
|
onclick={() =>
|
|
393
397
|
runBlock(() => editor.chain().focus().toggleTaskList().run())}
|
|
394
398
|
>
|
|
395
|
-
<CheckSquare size={14} />
|
|
399
|
+
<CheckSquare size={14} /> {t('taskList')}
|
|
396
400
|
<span class="hce-menu-shortcut">[] </span>
|
|
397
401
|
</button>
|
|
398
402
|
{/if}
|
|
@@ -409,7 +413,7 @@
|
|
|
409
413
|
onclick={() =>
|
|
410
414
|
runBlock(() => editor.chain().focus().toggleBlockquote().run())}
|
|
411
415
|
>
|
|
412
|
-
<Quote size={14} />
|
|
416
|
+
<Quote size={14} /> {t('blockquote')}
|
|
413
417
|
<span class="hce-menu-shortcut">" </span>
|
|
414
418
|
</button>
|
|
415
419
|
{/if}
|
|
@@ -423,7 +427,7 @@
|
|
|
423
427
|
onclick={() =>
|
|
424
428
|
runBlock(() => editor.chain().focus().setDetails().run())}
|
|
425
429
|
>
|
|
426
|
-
<ChevronRight size={14} />
|
|
430
|
+
<ChevronRight size={14} /> {t('toggle')}
|
|
427
431
|
<span class="hce-menu-shortcut">> </span>
|
|
428
432
|
</button>
|
|
429
433
|
<!--
|
|
@@ -450,7 +454,7 @@
|
|
|
450
454
|
.run(),
|
|
451
455
|
)}
|
|
452
456
|
>
|
|
453
|
-
<ToggleHeadingIcon size={14} level={level as 1 | 2 | 3} />
|
|
457
|
+
<ToggleHeadingIcon size={14} level={level as 1 | 2 | 3} /> {t('toggleHeading', { level })}
|
|
454
458
|
<span class="hce-menu-shortcut">{'#'.repeat(level)} > </span>
|
|
455
459
|
</button>
|
|
456
460
|
{/each}
|
|
@@ -475,7 +479,7 @@
|
|
|
475
479
|
<button
|
|
476
480
|
type="button"
|
|
477
481
|
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
478
|
-
aria-label=
|
|
482
|
+
aria-label={t('bold')}
|
|
479
483
|
class={cn(
|
|
480
484
|
"p-1.5 rounded-md transition-colors",
|
|
481
485
|
isActive("bold")
|
|
@@ -490,7 +494,7 @@
|
|
|
490
494
|
<button
|
|
491
495
|
type="button"
|
|
492
496
|
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
493
|
-
aria-label=
|
|
497
|
+
aria-label={t('italic')}
|
|
494
498
|
class={cn(
|
|
495
499
|
"p-1.5 rounded-md transition-colors",
|
|
496
500
|
isActive("italic")
|
|
@@ -505,7 +509,7 @@
|
|
|
505
509
|
<button
|
|
506
510
|
type="button"
|
|
507
511
|
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
508
|
-
aria-label=
|
|
512
|
+
aria-label={t('underline')}
|
|
509
513
|
class={cn(
|
|
510
514
|
"p-1.5 rounded-md transition-colors",
|
|
511
515
|
isActive("underline")
|
|
@@ -520,7 +524,7 @@
|
|
|
520
524
|
<button
|
|
521
525
|
type="button"
|
|
522
526
|
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
523
|
-
aria-label=
|
|
527
|
+
aria-label={t('strike')}
|
|
524
528
|
class={cn(
|
|
525
529
|
"p-1.5 rounded-md transition-colors",
|
|
526
530
|
isActive("strike")
|
|
@@ -552,7 +556,7 @@
|
|
|
552
556
|
<button
|
|
553
557
|
type="button"
|
|
554
558
|
onclick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
555
|
-
aria-label=
|
|
559
|
+
aria-label={t('alignLeft')}
|
|
556
560
|
class={cn(
|
|
557
561
|
"p-1.5 rounded-md transition-colors",
|
|
558
562
|
isActive({ textAlign: 'left' })
|
|
@@ -567,7 +571,7 @@
|
|
|
567
571
|
<button
|
|
568
572
|
type="button"
|
|
569
573
|
onclick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
570
|
-
aria-label=
|
|
574
|
+
aria-label={t('alignCenter')}
|
|
571
575
|
class={cn(
|
|
572
576
|
"p-1.5 rounded-md transition-colors",
|
|
573
577
|
isActive({ textAlign: 'center' })
|
|
@@ -582,7 +586,7 @@
|
|
|
582
586
|
<button
|
|
583
587
|
type="button"
|
|
584
588
|
onclick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
585
|
-
aria-label=
|
|
589
|
+
aria-label={t('alignRight')}
|
|
586
590
|
class={cn(
|
|
587
591
|
"p-1.5 rounded-md transition-colors",
|
|
588
592
|
isActive({ textAlign: 'right' })
|
|
@@ -603,11 +607,11 @@
|
|
|
603
607
|
<button
|
|
604
608
|
type="button"
|
|
605
609
|
onclick={() => (insertMenuOpen = !insertMenuOpen)}
|
|
606
|
-
aria-label=
|
|
610
|
+
aria-label={t('insert')}
|
|
607
611
|
class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
608
612
|
>
|
|
609
613
|
<Plus size={iconSize} />
|
|
610
|
-
<span class="text-sm"
|
|
614
|
+
<span class="text-sm">{t('insert')}</span>
|
|
611
615
|
<ChevronDown size={12} />
|
|
612
616
|
</button>
|
|
613
617
|
{#if insertMenuOpen}
|
|
@@ -622,7 +626,7 @@
|
|
|
622
626
|
onclick={() =>
|
|
623
627
|
runInsert(() => editor.chain().focus().setCodeBlock().run())}
|
|
624
628
|
>
|
|
625
|
-
<Code2 size={14} />
|
|
629
|
+
<Code2 size={14} /> {t('codeBlock')}
|
|
626
630
|
<span class="hce-menu-shortcut">```</span>
|
|
627
631
|
</button>
|
|
628
632
|
{/if}
|
|
@@ -633,7 +637,7 @@
|
|
|
633
637
|
onclick={() =>
|
|
634
638
|
runInsert(() => editor.chain().focus().promptMathDisplay().run())}
|
|
635
639
|
>
|
|
636
|
-
<Sigma size={14} />
|
|
640
|
+
<Sigma size={14} /> {t('mathBlock')}
|
|
637
641
|
<span class="hce-menu-shortcut">$$</span>
|
|
638
642
|
</button>
|
|
639
643
|
<!--
|
|
@@ -648,7 +652,7 @@
|
|
|
648
652
|
onclick={() =>
|
|
649
653
|
runInsert(() => editor.chain().focus().promptMathInline().run())}
|
|
650
654
|
>
|
|
651
|
-
<Sigma size={14} />
|
|
655
|
+
<Sigma size={14} /> {t('mathInline')}
|
|
652
656
|
<span class="hce-menu-shortcut">$ $</span>
|
|
653
657
|
</button>
|
|
654
658
|
{/if}
|
|
@@ -667,7 +671,7 @@
|
|
|
667
671
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
668
672
|
onclick={() => runInsert(onFileClick!)}
|
|
669
673
|
>
|
|
670
|
-
<Paperclip size={14} />
|
|
674
|
+
<Paperclip size={14} /> {t('file')}
|
|
671
675
|
</button>
|
|
672
676
|
{/if}
|
|
673
677
|
{#if has('table')}
|
|
@@ -679,7 +683,7 @@
|
|
|
679
683
|
insertTableSized(editor, { rows: 3, cols: 3, withHeaderRow: true }),
|
|
680
684
|
)}
|
|
681
685
|
>
|
|
682
|
-
<TableIcon size={14} />
|
|
686
|
+
<TableIcon size={14} /> {t('table3x3')}
|
|
683
687
|
</button>
|
|
684
688
|
{/if}
|
|
685
689
|
|
|
@@ -695,7 +699,7 @@
|
|
|
695
699
|
editor.chain().focus().setHorizontalRule().run(),
|
|
696
700
|
)}
|
|
697
701
|
>
|
|
698
|
-
<Minus size={14} />
|
|
702
|
+
<Minus size={14} /> {t('divider')}
|
|
699
703
|
<span class="hce-menu-shortcut">---</span>
|
|
700
704
|
</button>
|
|
701
705
|
{/if}
|
|
@@ -706,7 +710,7 @@
|
|
|
706
710
|
onclick={() =>
|
|
707
711
|
runInsert(() => editor.chain().focus().setColumns(2).run())}
|
|
708
712
|
>
|
|
709
|
-
<Columns2 size={14} />
|
|
713
|
+
<Columns2 size={14} /> {t('columns2')}
|
|
710
714
|
</button>
|
|
711
715
|
{/if}
|
|
712
716
|
{#if has('columns-3')}
|
|
@@ -716,7 +720,7 @@
|
|
|
716
720
|
onclick={() =>
|
|
717
721
|
runInsert(() => editor.chain().focus().setColumns(3).run())}
|
|
718
722
|
>
|
|
719
|
-
<Columns3 size={14} />
|
|
723
|
+
<Columns3 size={14} /> {t('columns3')}
|
|
720
724
|
</button>
|
|
721
725
|
{/if}
|
|
722
726
|
{#if has('tabs')}
|
|
@@ -726,7 +730,7 @@
|
|
|
726
730
|
onclick={() =>
|
|
727
731
|
runInsert(() => editor.chain().focus().setTabs(3).run())}
|
|
728
732
|
>
|
|
729
|
-
<PanelTop size={14} />
|
|
733
|
+
<PanelTop size={14} /> {t('tabs')}
|
|
730
734
|
</button>
|
|
731
735
|
{/if}
|
|
732
736
|
{#if has('card')}
|
|
@@ -735,7 +739,7 @@
|
|
|
735
739
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
736
740
|
onclick={() => runInsert(() => editor.chain().focus().setCard().run())}
|
|
737
741
|
>
|
|
738
|
-
<SquareDashed size={14} />
|
|
742
|
+
<SquareDashed size={14} /> {t('card')}
|
|
739
743
|
</button>
|
|
740
744
|
{/if}
|
|
741
745
|
|
|
@@ -748,7 +752,7 @@
|
|
|
748
752
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
749
753
|
onclick={addImage}
|
|
750
754
|
>
|
|
751
|
-
<ImageIcon size={14} />
|
|
755
|
+
<ImageIcon size={14} /> {t('image')}
|
|
752
756
|
</button>
|
|
753
757
|
{/if}
|
|
754
758
|
{#if has('video')}
|
|
@@ -761,7 +765,7 @@
|
|
|
761
765
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
762
766
|
onclick={addVideo}
|
|
763
767
|
>
|
|
764
|
-
<Youtube size={14} />
|
|
768
|
+
<Youtube size={14} /> {t('video')}
|
|
765
769
|
</button>
|
|
766
770
|
{/if}
|
|
767
771
|
{#if has('mbus')}
|
|
@@ -770,7 +774,7 @@
|
|
|
770
774
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
771
775
|
onclick={addMbus}
|
|
772
776
|
>
|
|
773
|
-
<Tv size={14} />
|
|
777
|
+
<Tv size={14} /> {t('mbusVideo')}
|
|
774
778
|
</button>
|
|
775
779
|
{/if}
|
|
776
780
|
</div>
|
|
@@ -783,24 +787,26 @@
|
|
|
783
787
|
<!-- (이미지 모달은 없다 — 에디터가 업로드/링크 탭 모달을 띄운다. `onImageClick` 참고.) -->
|
|
784
788
|
{#if modalState?.type === "video"}
|
|
785
789
|
<InputModal
|
|
786
|
-
title=
|
|
790
|
+
title={t('videoUrl')}
|
|
787
791
|
placeholder="https://www.youtube.com/watch?v=..."
|
|
788
792
|
onConfirm={(url) => {
|
|
789
793
|
editor.chain().focus().setVideoEmbed({ src: url }).run();
|
|
790
794
|
modalState = null;
|
|
791
795
|
}}
|
|
792
796
|
onCancel={() => (modalState = null)}
|
|
797
|
+
{t}
|
|
793
798
|
/>
|
|
794
799
|
{/if}
|
|
795
800
|
{#if modalState?.type === "mbus"}
|
|
796
801
|
<InputModal
|
|
797
|
-
title=
|
|
802
|
+
title={t('mbusVideoUrl')}
|
|
798
803
|
placeholder="https://play.mbus.tv/v1/hls/..."
|
|
799
804
|
onConfirm={(url) => {
|
|
800
805
|
editor.chain().focus().setMbusVideo({ src: url }).run();
|
|
801
806
|
modalState = null;
|
|
802
807
|
}}
|
|
803
808
|
onCancel={() => (modalState = null)}
|
|
809
|
+
{t}
|
|
804
810
|
/>
|
|
805
811
|
{/if}
|
|
806
812
|
|