@teriusu/rich-editor 0.7.1 → 0.7.2
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 +169 -118
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +410 -814
- package/dist/components/FixedToolbar.svelte.d.ts +0 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +28 -25
- 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,153 @@
|
|
|
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"
|
|
126
175
|
>
|
|
127
176
|
<button
|
|
128
177
|
type="button"
|
|
129
178
|
class={cn(
|
|
130
179
|
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
131
|
-
|
|
132
|
-
? "
|
|
180
|
+
isParagraphActive()
|
|
181
|
+
? "bg-primary/10 text-primary"
|
|
133
182
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
134
183
|
)}
|
|
135
184
|
onclick={() => {
|
|
136
185
|
editor.chain().focus().setParagraph().run();
|
|
137
|
-
|
|
186
|
+
showBlockMenu = false;
|
|
138
187
|
}}
|
|
139
188
|
>
|
|
140
189
|
<Type size={12} /> 본문
|
|
141
190
|
</button>
|
|
142
191
|
{#each [1, 2, 3] as level}
|
|
143
192
|
{#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
|
|
144
|
-
|
|
193
|
+
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
class={cn(
|
|
197
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
198
|
+
editor.isActive("heading", { level })
|
|
199
|
+
? "bg-primary/10 text-primary"
|
|
200
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
201
|
+
)}
|
|
202
|
+
onclick={() => {
|
|
203
|
+
editor
|
|
204
|
+
.chain()
|
|
205
|
+
.focus()
|
|
206
|
+
.toggleHeading({ level: level as 1 | 2 | 3 })
|
|
207
|
+
.run();
|
|
208
|
+
showBlockMenu = false;
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<Icon size={12} /> 제목 {level}
|
|
212
|
+
</button>
|
|
213
|
+
{/if}
|
|
214
|
+
{/each}
|
|
215
|
+
{#if has('bullet-list')}
|
|
145
216
|
<button
|
|
146
217
|
type="button"
|
|
147
218
|
class={cn(
|
|
148
219
|
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
149
|
-
editor.isActive("
|
|
150
|
-
? "
|
|
220
|
+
editor.isActive("bulletList")
|
|
221
|
+
? "bg-primary/10 text-primary"
|
|
151
222
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
152
223
|
)}
|
|
153
224
|
onclick={() => {
|
|
154
|
-
editor
|
|
155
|
-
|
|
156
|
-
.focus()
|
|
157
|
-
.toggleHeading({ level: level as 1 | 2 | 3 })
|
|
158
|
-
.run();
|
|
159
|
-
showHeadings = false;
|
|
225
|
+
editor.chain().focus().toggleBulletList().run();
|
|
226
|
+
showBlockMenu = false;
|
|
160
227
|
}}
|
|
161
228
|
>
|
|
162
|
-
<
|
|
229
|
+
<List size={12} /> 글머리 목록
|
|
163
230
|
</button>
|
|
164
|
-
|
|
165
|
-
{
|
|
231
|
+
{/if}
|
|
232
|
+
{#if has('ordered-list')}
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class={cn(
|
|
236
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
237
|
+
editor.isActive("orderedList")
|
|
238
|
+
? "bg-primary/10 text-primary"
|
|
239
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
240
|
+
)}
|
|
241
|
+
onclick={() => {
|
|
242
|
+
editor.chain().focus().toggleOrderedList().run();
|
|
243
|
+
showBlockMenu = false;
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<ListOrdered size={12} /> 번호 목록
|
|
247
|
+
</button>
|
|
248
|
+
{/if}
|
|
249
|
+
{#if has('checklist')}
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
class={cn(
|
|
253
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
254
|
+
editor.isActive("taskList")
|
|
255
|
+
? "bg-primary/10 text-primary"
|
|
256
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
257
|
+
)}
|
|
258
|
+
onclick={() => {
|
|
259
|
+
editor.chain().focus().toggleTaskList().run();
|
|
260
|
+
showBlockMenu = false;
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
<ListChecks size={12} /> 체크리스트
|
|
264
|
+
</button>
|
|
265
|
+
{/if}
|
|
266
|
+
{#if has('blockquote')}
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class={cn(
|
|
270
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
271
|
+
editor.isActive("blockquote")
|
|
272
|
+
? "bg-primary/10 text-primary"
|
|
273
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
274
|
+
)}
|
|
275
|
+
onclick={() => {
|
|
276
|
+
editor.chain().focus().toggleBlockquote().run();
|
|
277
|
+
showBlockMenu = false;
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
<Quote size={12} /> 인용문
|
|
281
|
+
</button>
|
|
282
|
+
{/if}
|
|
166
283
|
</div>
|
|
167
284
|
{/if}
|
|
168
285
|
</div>
|
|
@@ -235,8 +352,29 @@
|
|
|
235
352
|
<Strikethrough size={iconSize} />
|
|
236
353
|
</button>
|
|
237
354
|
{/if}
|
|
355
|
+
|
|
356
|
+
{#if has('highlight') || has('text-color')}
|
|
357
|
+
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
358
|
+
{/if}
|
|
359
|
+
|
|
360
|
+
{#if has('highlight')}
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
364
|
+
title="하이라이트"
|
|
365
|
+
aria-label="하이라이트"
|
|
366
|
+
class={cn(
|
|
367
|
+
"p-1.5 rounded-full transition-colors",
|
|
368
|
+
editor.isActive("highlight")
|
|
369
|
+
? "bg-white/20 text-white"
|
|
370
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
371
|
+
)}
|
|
372
|
+
>
|
|
373
|
+
<Highlighter size={iconSize} />
|
|
374
|
+
</button>
|
|
375
|
+
{/if}
|
|
238
376
|
{#if has('text-color')}
|
|
239
|
-
<div class="relative">
|
|
377
|
+
<div class="relative" bind:this={colorMenuEl}>
|
|
240
378
|
<button
|
|
241
379
|
type="button"
|
|
242
380
|
onclick={() => (showColors = !showColors)}
|
|
@@ -295,110 +433,23 @@
|
|
|
295
433
|
{/if}
|
|
296
434
|
</div>
|
|
297
435
|
{/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
436
|
|
|
331
437
|
{#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
438
|
<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
439
|
<button
|
|
388
440
|
type="button"
|
|
389
|
-
onclick={
|
|
390
|
-
title="
|
|
391
|
-
aria-label="
|
|
441
|
+
onclick={addLink}
|
|
442
|
+
title="링크"
|
|
443
|
+
aria-label="링크"
|
|
392
444
|
class={cn(
|
|
393
445
|
"p-1.5 rounded-full transition-colors",
|
|
394
|
-
editor.isActive(
|
|
446
|
+
editor.isActive("link")
|
|
395
447
|
? "bg-white/20 text-white"
|
|
396
448
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
397
449
|
)}
|
|
398
450
|
>
|
|
399
|
-
<
|
|
451
|
+
<LinkIcon size={iconSize} />
|
|
400
452
|
</button>
|
|
401
|
-
{/if}
|
|
402
453
|
{/if}
|
|
403
454
|
</div>
|
|
404
455
|
</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"}
|