@teriusu/rich-editor 0.7.1 → 0.7.3
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 +175 -118
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +486 -805
- package/dist/components/FixedToolbar.svelte.d.ts +0 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +43 -40
- package/dist/components/SlashCommandMenu.svelte.d.ts +1 -1
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +2 -64
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/Columns.d.ts.map +1 -1
- package/dist/extensions/Columns.js +35 -0
- package/dist/extensions/FixedDetails.d.ts.map +1 -1
- package/dist/extensions/FixedDetails.js +11 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -7
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +0 -3
- package/dist/utils/sanitize.test.js +0 -7
- package/package.json +1 -2
- package/dist/extensions/VideoBlock.d.ts +0 -16
- package/dist/extensions/VideoBlock.d.ts.map +0 -1
- package/dist/extensions/VideoBlock.js +0 -87
- package/dist/extensions/VideoBlock.test.d.ts +0 -2
- package/dist/extensions/VideoBlock.test.d.ts.map +0 -1
- package/dist/extensions/VideoBlock.test.js +0 -54
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
Strikethrough,
|
|
13
13
|
Highlighter,
|
|
14
14
|
LinkIcon,
|
|
15
|
-
AlignLeft,
|
|
16
|
-
AlignCenter,
|
|
17
|
-
AlignRight,
|
|
18
15
|
Heading1,
|
|
19
16
|
Heading2,
|
|
20
17
|
Heading3,
|
|
21
18
|
Type,
|
|
22
|
-
Code,
|
|
23
19
|
Palette,
|
|
20
|
+
List,
|
|
21
|
+
ListOrdered,
|
|
22
|
+
ListChecks,
|
|
23
|
+
Quote,
|
|
24
|
+
ChevronDown,
|
|
24
25
|
} from "lucide-svelte";
|
|
25
26
|
import { cn } from "../utils/cn";
|
|
26
27
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
@@ -37,9 +38,11 @@
|
|
|
37
38
|
|
|
38
39
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
39
40
|
|
|
40
|
-
let
|
|
41
|
+
let showBlockMenu = $state(false);
|
|
41
42
|
let showColors = $state(false);
|
|
42
43
|
let menuEl: HTMLDivElement | undefined = $state();
|
|
44
|
+
let blockMenuEl: HTMLDivElement | undefined = $state();
|
|
45
|
+
let colorMenuEl: HTMLDivElement | undefined = $state();
|
|
43
46
|
const iconSize = 14;
|
|
44
47
|
|
|
45
48
|
const TEXT_COLORS = [
|
|
@@ -58,9 +61,23 @@
|
|
|
58
61
|
if (editor.isActive("heading", { level: 1 })) return "제목 1";
|
|
59
62
|
if (editor.isActive("heading", { level: 2 })) return "제목 2";
|
|
60
63
|
if (editor.isActive("heading", { level: 3 })) return "제목 3";
|
|
64
|
+
if (editor.isActive("bulletList")) return "글머리 목록";
|
|
65
|
+
if (editor.isActive("orderedList")) return "번호 목록";
|
|
66
|
+
if (editor.isActive("taskList")) return "체크리스트";
|
|
67
|
+
if (editor.isActive("blockquote")) return "인용문";
|
|
61
68
|
return "본문";
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
function isParagraphActive(): boolean {
|
|
72
|
+
return (
|
|
73
|
+
!editor.isActive("heading") &&
|
|
74
|
+
!editor.isActive("bulletList") &&
|
|
75
|
+
!editor.isActive("orderedList") &&
|
|
76
|
+
!editor.isActive("taskList") &&
|
|
77
|
+
!editor.isActive("blockquote")
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
async function addLink() {
|
|
65
82
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
66
83
|
const url = onPromptLink
|
|
@@ -79,6 +96,23 @@
|
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
function handleDocClick(e: MouseEvent) {
|
|
100
|
+
const target = e.target as Node;
|
|
101
|
+
if (showBlockMenu && blockMenuEl && !blockMenuEl.contains(target)) {
|
|
102
|
+
showBlockMenu = false;
|
|
103
|
+
}
|
|
104
|
+
if (showColors && colorMenuEl && !colorMenuEl.contains(target)) {
|
|
105
|
+
showColors = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
110
|
+
if (e.key === "Escape") {
|
|
111
|
+
showBlockMenu = false;
|
|
112
|
+
showColors = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
onMount(() => {
|
|
83
117
|
if (!menuEl) return;
|
|
84
118
|
|
|
@@ -99,70 +133,156 @@
|
|
|
99
133
|
});
|
|
100
134
|
|
|
101
135
|
editor.registerPlugin(plugin);
|
|
136
|
+
document.addEventListener("mousedown", handleDocClick);
|
|
137
|
+
document.addEventListener("keydown", handleKeydown);
|
|
102
138
|
|
|
103
139
|
return () => {
|
|
104
140
|
editor.unregisterPlugin(bubbleToolbarKey);
|
|
141
|
+
document.removeEventListener("mousedown", handleDocClick);
|
|
142
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
105
143
|
};
|
|
106
144
|
});
|
|
145
|
+
|
|
146
|
+
const hasBlockMenu = $derived(
|
|
147
|
+
has('h1') ||
|
|
148
|
+
has('h2') ||
|
|
149
|
+
has('h3') ||
|
|
150
|
+
has('bullet-list') ||
|
|
151
|
+
has('ordered-list') ||
|
|
152
|
+
has('checklist') ||
|
|
153
|
+
has('blockquote'),
|
|
154
|
+
);
|
|
107
155
|
</script>
|
|
108
156
|
|
|
109
157
|
<div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
|
|
110
158
|
<div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-full shadow-xl">
|
|
111
|
-
{#if
|
|
159
|
+
{#if hasBlockMenu}
|
|
112
160
|
<!-- Block type selector -->
|
|
113
|
-
<div class="relative">
|
|
161
|
+
<div class="relative" bind:this={blockMenuEl}>
|
|
114
162
|
<button
|
|
115
163
|
type="button"
|
|
116
|
-
onclick={() => (
|
|
164
|
+
onclick={() => (showBlockMenu = !showBlockMenu)}
|
|
117
165
|
class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-white/80 hover:text-white hover:bg-white/10 transition-colors"
|
|
118
166
|
>
|
|
119
167
|
<Type size={12} />
|
|
120
168
|
{getCurrentBlockLabel()}
|
|
169
|
+
<ChevronDown size={12} />
|
|
121
170
|
</button>
|
|
122
|
-
{#if
|
|
171
|
+
{#if showBlockMenu}
|
|
123
172
|
<div
|
|
124
173
|
class="absolute bottom-full left-0 mb-1 bg-foreground rounded-lg shadow-xl border border-white/10 py-1"
|
|
125
|
-
style="min-width:
|
|
174
|
+
style="min-width: 140px"
|
|
175
|
+
onmousedown={(e) => e.preventDefault()}
|
|
176
|
+
role="menu"
|
|
177
|
+
tabindex="-1"
|
|
126
178
|
>
|
|
127
179
|
<button
|
|
128
180
|
type="button"
|
|
129
181
|
class={cn(
|
|
130
182
|
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
131
|
-
|
|
132
|
-
? "
|
|
183
|
+
isParagraphActive()
|
|
184
|
+
? "bg-primary/10 text-primary"
|
|
133
185
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
134
186
|
)}
|
|
135
187
|
onclick={() => {
|
|
136
188
|
editor.chain().focus().setParagraph().run();
|
|
137
|
-
|
|
189
|
+
showBlockMenu = false;
|
|
138
190
|
}}
|
|
139
191
|
>
|
|
140
192
|
<Type size={12} /> 본문
|
|
141
193
|
</button>
|
|
142
194
|
{#each [1, 2, 3] as level}
|
|
143
195
|
{#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
|
|
144
|
-
|
|
196
|
+
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
class={cn(
|
|
200
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
201
|
+
editor.isActive("heading", { level })
|
|
202
|
+
? "bg-primary/10 text-primary"
|
|
203
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
204
|
+
)}
|
|
205
|
+
onclick={() => {
|
|
206
|
+
editor
|
|
207
|
+
.chain()
|
|
208
|
+
.focus()
|
|
209
|
+
.toggleHeading({ level: level as 1 | 2 | 3 })
|
|
210
|
+
.run();
|
|
211
|
+
showBlockMenu = false;
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<Icon size={12} /> 제목 {level}
|
|
215
|
+
</button>
|
|
216
|
+
{/if}
|
|
217
|
+
{/each}
|
|
218
|
+
{#if has('bullet-list')}
|
|
145
219
|
<button
|
|
146
220
|
type="button"
|
|
147
221
|
class={cn(
|
|
148
222
|
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
149
|
-
editor.isActive("
|
|
150
|
-
? "
|
|
223
|
+
editor.isActive("bulletList")
|
|
224
|
+
? "bg-primary/10 text-primary"
|
|
151
225
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
152
226
|
)}
|
|
153
227
|
onclick={() => {
|
|
154
|
-
editor
|
|
155
|
-
|
|
156
|
-
.focus()
|
|
157
|
-
.toggleHeading({ level: level as 1 | 2 | 3 })
|
|
158
|
-
.run();
|
|
159
|
-
showHeadings = false;
|
|
228
|
+
editor.chain().focus().toggleBulletList().run();
|
|
229
|
+
showBlockMenu = false;
|
|
160
230
|
}}
|
|
161
231
|
>
|
|
162
|
-
<
|
|
232
|
+
<List size={12} /> 글머리 목록
|
|
163
233
|
</button>
|
|
164
|
-
|
|
165
|
-
{
|
|
234
|
+
{/if}
|
|
235
|
+
{#if has('ordered-list')}
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
class={cn(
|
|
239
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
240
|
+
editor.isActive("orderedList")
|
|
241
|
+
? "bg-primary/10 text-primary"
|
|
242
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
243
|
+
)}
|
|
244
|
+
onclick={() => {
|
|
245
|
+
editor.chain().focus().toggleOrderedList().run();
|
|
246
|
+
showBlockMenu = false;
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<ListOrdered size={12} /> 번호 목록
|
|
250
|
+
</button>
|
|
251
|
+
{/if}
|
|
252
|
+
{#if has('checklist')}
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
class={cn(
|
|
256
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
257
|
+
editor.isActive("taskList")
|
|
258
|
+
? "bg-primary/10 text-primary"
|
|
259
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
260
|
+
)}
|
|
261
|
+
onclick={() => {
|
|
262
|
+
editor.chain().focus().toggleTaskList().run();
|
|
263
|
+
showBlockMenu = false;
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<ListChecks size={12} /> 체크리스트
|
|
267
|
+
</button>
|
|
268
|
+
{/if}
|
|
269
|
+
{#if has('blockquote')}
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
class={cn(
|
|
273
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
274
|
+
editor.isActive("blockquote")
|
|
275
|
+
? "bg-primary/10 text-primary"
|
|
276
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
277
|
+
)}
|
|
278
|
+
onclick={() => {
|
|
279
|
+
editor.chain().focus().toggleBlockquote().run();
|
|
280
|
+
showBlockMenu = false;
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<Quote size={12} /> 인용문
|
|
284
|
+
</button>
|
|
285
|
+
{/if}
|
|
166
286
|
</div>
|
|
167
287
|
{/if}
|
|
168
288
|
</div>
|
|
@@ -235,8 +355,29 @@
|
|
|
235
355
|
<Strikethrough size={iconSize} />
|
|
236
356
|
</button>
|
|
237
357
|
{/if}
|
|
358
|
+
|
|
359
|
+
{#if has('highlight') || has('text-color')}
|
|
360
|
+
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
361
|
+
{/if}
|
|
362
|
+
|
|
363
|
+
{#if has('highlight')}
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
367
|
+
title="하이라이트"
|
|
368
|
+
aria-label="하이라이트"
|
|
369
|
+
class={cn(
|
|
370
|
+
"p-1.5 rounded-full transition-colors",
|
|
371
|
+
editor.isActive("highlight")
|
|
372
|
+
? "bg-white/20 text-white"
|
|
373
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
374
|
+
)}
|
|
375
|
+
>
|
|
376
|
+
<Highlighter size={iconSize} />
|
|
377
|
+
</button>
|
|
378
|
+
{/if}
|
|
238
379
|
{#if has('text-color')}
|
|
239
|
-
<div class="relative">
|
|
380
|
+
<div class="relative" bind:this={colorMenuEl}>
|
|
240
381
|
<button
|
|
241
382
|
type="button"
|
|
242
383
|
onclick={() => (showColors = !showColors)}
|
|
@@ -255,6 +396,9 @@
|
|
|
255
396
|
<div
|
|
256
397
|
class="absolute bottom-full left-0 mb-1 bg-foreground rounded-lg shadow-xl border border-white/10 p-2"
|
|
257
398
|
style="min-width: 160px"
|
|
399
|
+
onmousedown={(e) => e.preventDefault()}
|
|
400
|
+
role="menu"
|
|
401
|
+
tabindex="-1"
|
|
258
402
|
>
|
|
259
403
|
<div class="grid grid-cols-3 gap-1.5">
|
|
260
404
|
{#each TEXT_COLORS as c}
|
|
@@ -295,110 +439,23 @@
|
|
|
295
439
|
{/if}
|
|
296
440
|
</div>
|
|
297
441
|
{/if}
|
|
298
|
-
{#if has('highlight')}
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
302
|
-
title="하이라이트"
|
|
303
|
-
aria-label="하이라이트"
|
|
304
|
-
class={cn(
|
|
305
|
-
"p-1.5 rounded-full transition-colors",
|
|
306
|
-
editor.isActive("highlight")
|
|
307
|
-
? "bg-white/20 text-white"
|
|
308
|
-
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
309
|
-
)}
|
|
310
|
-
>
|
|
311
|
-
<Highlighter size={iconSize} />
|
|
312
|
-
</button>
|
|
313
|
-
{/if}
|
|
314
|
-
{#if has('code')}
|
|
315
|
-
<button
|
|
316
|
-
type="button"
|
|
317
|
-
onclick={() => editor.chain().focus().toggleCode().run()}
|
|
318
|
-
title="코드"
|
|
319
|
-
aria-label="코드"
|
|
320
|
-
class={cn(
|
|
321
|
-
"p-1.5 rounded-full transition-colors",
|
|
322
|
-
editor.isActive("code")
|
|
323
|
-
? "bg-white/20 text-white"
|
|
324
|
-
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
325
|
-
)}
|
|
326
|
-
>
|
|
327
|
-
<Code size={iconSize} />
|
|
328
|
-
</button>
|
|
329
|
-
{/if}
|
|
330
442
|
|
|
331
443
|
{#if has('link')}
|
|
332
|
-
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
333
|
-
|
|
334
|
-
<button
|
|
335
|
-
type="button"
|
|
336
|
-
onclick={addLink}
|
|
337
|
-
title="링크"
|
|
338
|
-
aria-label="링크"
|
|
339
|
-
class={cn(
|
|
340
|
-
"p-1.5 rounded-full transition-colors",
|
|
341
|
-
editor.isActive("link")
|
|
342
|
-
? "bg-white/20 text-white"
|
|
343
|
-
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
344
|
-
)}
|
|
345
|
-
>
|
|
346
|
-
<LinkIcon size={iconSize} />
|
|
347
|
-
</button>
|
|
348
|
-
{/if}
|
|
349
|
-
|
|
350
|
-
{#if has('align-left') || has('align-center') || has('align-right')}
|
|
351
444
|
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
352
|
-
|
|
353
|
-
<!-- Alignment -->
|
|
354
|
-
{#if has('align-left')}
|
|
355
|
-
<button
|
|
356
|
-
type="button"
|
|
357
|
-
onclick={() => editor.chain().focus().setTextAlign("left").run()}
|
|
358
|
-
title="왼쪽 정렬"
|
|
359
|
-
aria-label="왼쪽 정렬"
|
|
360
|
-
class={cn(
|
|
361
|
-
"p-1.5 rounded-full transition-colors",
|
|
362
|
-
editor.isActive({ textAlign: "left" })
|
|
363
|
-
? "bg-white/20 text-white"
|
|
364
|
-
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
365
|
-
)}
|
|
366
|
-
>
|
|
367
|
-
<AlignLeft size={iconSize} />
|
|
368
|
-
</button>
|
|
369
|
-
{/if}
|
|
370
|
-
{#if has('align-center')}
|
|
371
|
-
<button
|
|
372
|
-
type="button"
|
|
373
|
-
onclick={() => editor.chain().focus().setTextAlign("center").run()}
|
|
374
|
-
title="가운데 정렬"
|
|
375
|
-
aria-label="가운데 정렬"
|
|
376
|
-
class={cn(
|
|
377
|
-
"p-1.5 rounded-full transition-colors",
|
|
378
|
-
editor.isActive({ textAlign: "center" })
|
|
379
|
-
? "bg-white/20 text-white"
|
|
380
|
-
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
381
|
-
)}
|
|
382
|
-
>
|
|
383
|
-
<AlignCenter size={iconSize} />
|
|
384
|
-
</button>
|
|
385
|
-
{/if}
|
|
386
|
-
{#if has('align-right')}
|
|
387
445
|
<button
|
|
388
446
|
type="button"
|
|
389
|
-
onclick={
|
|
390
|
-
title="
|
|
391
|
-
aria-label="
|
|
447
|
+
onclick={addLink}
|
|
448
|
+
title="링크"
|
|
449
|
+
aria-label="링크"
|
|
392
450
|
class={cn(
|
|
393
451
|
"p-1.5 rounded-full transition-colors",
|
|
394
|
-
editor.isActive(
|
|
452
|
+
editor.isActive("link")
|
|
395
453
|
? "bg-white/20 text-white"
|
|
396
454
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
397
455
|
)}
|
|
398
456
|
>
|
|
399
|
-
<
|
|
457
|
+
<LinkIcon size={iconSize} />
|
|
400
458
|
</button>
|
|
401
|
-
{/if}
|
|
402
459
|
{/if}
|
|
403
460
|
</div>
|
|
404
461
|
</div>
|
|
@@ -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;
|
|
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;AAuB3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AA4VJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|