@teriusu/rich-editor 0.9.5 → 0.9.7
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 +102 -49
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +136 -214
- package/dist/components/FixedToolbar.svelte.d.ts +9 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/InputModal.svelte +1 -1
- package/dist/components/MathModal.svelte +111 -1
- package/dist/components/MathModal.svelte.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.svelte +206 -0
- package/dist/components/MediaPickerModal.svelte.d.ts +18 -0
- package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +114 -13
- package/dist/components/SlashCommandMenu.svelte.d.ts +10 -1
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TableBubbleMenu.svelte +71 -48
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +180 -24
- package/dist/components/TipTapEditor.svelte.d.ts +2 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/NotionInputRules.d.ts +59 -0
- package/dist/extensions/NotionInputRules.d.ts.map +1 -0
- package/dist/extensions/NotionInputRules.js +141 -0
- package/dist/extensions/NotionInputRules.test.d.ts +2 -0
- package/dist/extensions/NotionInputRules.test.d.ts.map +1 -0
- package/dist/extensions/NotionInputRules.test.js +117 -0
- package/dist/extensions/VideoEmbed.d.ts +36 -0
- package/dist/extensions/VideoEmbed.d.ts.map +1 -0
- package/dist/extensions/VideoEmbed.js +246 -0
- package/dist/extensions/VideoEmbed.test.d.ts +2 -0
- package/dist/extensions/VideoEmbed.test.d.ts.map +1 -0
- package/dist/extensions/VideoEmbed.test.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +20 -6
- package/dist/utils/lowlight.d.ts +17 -0
- package/dist/utils/lowlight.d.ts.map +1 -0
- package/dist/utils/lowlight.js +106 -0
- package/dist/utils/lowlight.test.d.ts +2 -0
- package/dist/utils/lowlight.test.d.ts.map +1 -0
- package/dist/utils/lowlight.test.js +56 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +14 -0
- package/dist/utils/splitCell.d.ts +20 -0
- package/dist/utils/splitCell.d.ts.map +1 -0
- package/dist/utils/splitCell.js +74 -0
- package/dist/utils/table.d.ts +22 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +68 -0
- package/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AA0bJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
} from "lucide-svelte";
|
|
19
19
|
import { onMount } from "svelte";
|
|
20
20
|
import { cn } from "../utils/cn";
|
|
21
|
+
import { splitCellPreservingHeaders } from "../utils/splitCell";
|
|
21
22
|
|
|
22
23
|
const PRESET_COLORS = [
|
|
23
24
|
{ label: "없음", value: "" },
|
|
@@ -94,11 +95,29 @@
|
|
|
94
95
|
visible = true;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/*
|
|
99
|
+
* ⚠️ **버튼의 활성 여부가 갱신되게 하는 장치.**
|
|
100
|
+
*
|
|
101
|
+
* `editor` 는 TipTap 이 만든 평범한 객체라 `$state` 가 아니다. 그래서 `editor.can()` 을
|
|
102
|
+
* 그냥 부르면 Svelte 가 의존성을 못 보고 **처음 값에 굳는다** — 셀을 여러 개 골라도
|
|
103
|
+
* `mergeCells()` 가능 여부가 다시 계산되지 않아 **`셀 병합`·`셀 분할` 이 영영 비활성**
|
|
104
|
+
* 이었다(사용자 지적: "표에서 셀병합 분할이 안되네?").
|
|
105
|
+
*
|
|
106
|
+
* 고정 툴바가 같은 이유로 이미 쓰고 있는 방식이다 — 트랜잭션마다 카운터를 올리고
|
|
107
|
+
* 활성 여부를 읽는 자리에서 그 카운터를 함께 읽는다(쉼표 연산자).
|
|
108
|
+
*/
|
|
109
|
+
let tick = $state(0);
|
|
110
|
+
const canDo = (fn: (c: ReturnType<Editor["can"]>) => boolean) =>
|
|
111
|
+
(tick, fn(editor.can()));
|
|
112
|
+
|
|
97
113
|
onMount(() => {
|
|
114
|
+
const bump = () => tick++;
|
|
115
|
+
editor.on("transaction", bump);
|
|
98
116
|
editor.on("selectionUpdate", updatePosition);
|
|
99
117
|
editor.on("update", updatePosition);
|
|
100
118
|
|
|
101
119
|
return () => {
|
|
120
|
+
editor.off("transaction", bump);
|
|
102
121
|
editor.off("selectionUpdate", updatePosition);
|
|
103
122
|
editor.off("update", updatePosition);
|
|
104
123
|
};
|
|
@@ -121,21 +140,25 @@
|
|
|
121
140
|
<div
|
|
122
141
|
bind:this={menuEl}
|
|
123
142
|
class="absolute hce-table-bubble"
|
|
143
|
+
onmousedown={(e) => e.preventDefault()}
|
|
144
|
+
role="toolbar"
|
|
145
|
+
tabindex="-1"
|
|
146
|
+
aria-label="표 편집"
|
|
124
147
|
style="top: {pos.top}px; left: {pos.left}px; width: {pos.width}px"
|
|
125
148
|
>
|
|
126
149
|
<div
|
|
127
|
-
class="flex items-center justify-center gap-0.5 px-1.5 py-1
|
|
150
|
+
class="flex items-center justify-center gap-0.5 px-1.5 py-1 bg-popover border border-border rounded-full shadow-xl w-fit mx-auto"
|
|
128
151
|
>
|
|
129
152
|
<!-- Row/Col buttons -->
|
|
130
153
|
<div class="hce-table-btn-wrap">
|
|
131
154
|
<button
|
|
132
155
|
type="button"
|
|
133
156
|
onclick={() => editor.chain().focus().addRowBefore().run()}
|
|
134
|
-
disabled={!
|
|
157
|
+
disabled={!canDo((c) => c.addRowBefore())}
|
|
135
158
|
class={cn(
|
|
136
159
|
"p-1.5 rounded-full transition-colors",
|
|
137
|
-
"text-
|
|
138
|
-
!
|
|
160
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
161
|
+
!canDo((c) => c.addRowBefore()) && "opacity-30 pointer-events-none",
|
|
139
162
|
)}
|
|
140
163
|
>
|
|
141
164
|
<ArrowUpToLine size={iconSize} />
|
|
@@ -147,11 +170,11 @@
|
|
|
147
170
|
<button
|
|
148
171
|
type="button"
|
|
149
172
|
onclick={() => editor.chain().focus().addRowAfter().run()}
|
|
150
|
-
disabled={!
|
|
173
|
+
disabled={!canDo((c) => c.addRowAfter())}
|
|
151
174
|
class={cn(
|
|
152
175
|
"p-1.5 rounded-full transition-colors",
|
|
153
|
-
"text-
|
|
154
|
-
!
|
|
176
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
177
|
+
!canDo((c) => c.addRowAfter()) && "opacity-30 pointer-events-none",
|
|
155
178
|
)}
|
|
156
179
|
>
|
|
157
180
|
<ArrowDownToLine size={iconSize} />
|
|
@@ -163,11 +186,11 @@
|
|
|
163
186
|
<button
|
|
164
187
|
type="button"
|
|
165
188
|
onclick={() => editor.chain().focus().addColumnBefore().run()}
|
|
166
|
-
disabled={!
|
|
189
|
+
disabled={!canDo((c) => c.addColumnBefore())}
|
|
167
190
|
class={cn(
|
|
168
191
|
"p-1.5 rounded-full transition-colors",
|
|
169
|
-
"text-
|
|
170
|
-
!
|
|
192
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
193
|
+
!canDo((c) => c.addColumnBefore()) &&
|
|
171
194
|
"opacity-30 pointer-events-none",
|
|
172
195
|
)}
|
|
173
196
|
>
|
|
@@ -180,11 +203,11 @@
|
|
|
180
203
|
<button
|
|
181
204
|
type="button"
|
|
182
205
|
onclick={() => editor.chain().focus().addColumnAfter().run()}
|
|
183
|
-
disabled={!
|
|
206
|
+
disabled={!canDo((c) => c.addColumnAfter())}
|
|
184
207
|
class={cn(
|
|
185
208
|
"p-1.5 rounded-full transition-colors",
|
|
186
|
-
"text-
|
|
187
|
-
!
|
|
209
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
210
|
+
!canDo((c) => c.addColumnAfter()) &&
|
|
188
211
|
"opacity-30 pointer-events-none",
|
|
189
212
|
)}
|
|
190
213
|
>
|
|
@@ -193,17 +216,17 @@
|
|
|
193
216
|
<span class="hce-table-btn-tooltip">열 오른쪽에 추가</span>
|
|
194
217
|
</div>
|
|
195
218
|
|
|
196
|
-
<div class="w-px h-5 bg-
|
|
219
|
+
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
197
220
|
|
|
198
221
|
<div class="hce-table-btn-wrap">
|
|
199
222
|
<button
|
|
200
223
|
type="button"
|
|
201
224
|
onclick={() => editor.chain().focus().toggleHeaderRow().run()}
|
|
202
|
-
disabled={!
|
|
225
|
+
disabled={!canDo((c) => c.toggleHeaderRow())}
|
|
203
226
|
class={cn(
|
|
204
227
|
"p-1.5 rounded-full transition-colors",
|
|
205
|
-
"text-
|
|
206
|
-
!
|
|
228
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
229
|
+
!canDo((c) => c.toggleHeaderRow()) &&
|
|
207
230
|
"opacity-30 pointer-events-none",
|
|
208
231
|
)}
|
|
209
232
|
>
|
|
@@ -216,11 +239,11 @@
|
|
|
216
239
|
<button
|
|
217
240
|
type="button"
|
|
218
241
|
onclick={() => editor.chain().focus().toggleHeaderColumn().run()}
|
|
219
|
-
disabled={!
|
|
242
|
+
disabled={!canDo((c) => c.toggleHeaderColumn())}
|
|
220
243
|
class={cn(
|
|
221
244
|
"p-1.5 rounded-full transition-colors",
|
|
222
|
-
"text-
|
|
223
|
-
!
|
|
245
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
246
|
+
!canDo((c) => c.toggleHeaderColumn()) &&
|
|
224
247
|
"opacity-30 pointer-events-none",
|
|
225
248
|
)}
|
|
226
249
|
>
|
|
@@ -233,11 +256,11 @@
|
|
|
233
256
|
<button
|
|
234
257
|
type="button"
|
|
235
258
|
onclick={() => editor.chain().focus().mergeCells().run()}
|
|
236
|
-
disabled={!
|
|
259
|
+
disabled={!canDo((c) => c.mergeCells())}
|
|
237
260
|
class={cn(
|
|
238
261
|
"p-1.5 rounded-full transition-colors",
|
|
239
|
-
"text-
|
|
240
|
-
!
|
|
262
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
263
|
+
!canDo((c) => c.mergeCells()) && "opacity-30 pointer-events-none",
|
|
241
264
|
)}
|
|
242
265
|
>
|
|
243
266
|
<Combine size={iconSize} />
|
|
@@ -248,12 +271,12 @@
|
|
|
248
271
|
<div class="hce-table-btn-wrap">
|
|
249
272
|
<button
|
|
250
273
|
type="button"
|
|
251
|
-
onclick={() => editor
|
|
252
|
-
disabled={!
|
|
274
|
+
onclick={() => splitCellPreservingHeaders(editor)}
|
|
275
|
+
disabled={!canDo((c) => c.splitCell())}
|
|
253
276
|
class={cn(
|
|
254
277
|
"p-1.5 rounded-full transition-colors",
|
|
255
|
-
"text-
|
|
256
|
-
!
|
|
278
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
279
|
+
!canDo((c) => c.splitCell()) && "opacity-30 pointer-events-none",
|
|
257
280
|
)}
|
|
258
281
|
>
|
|
259
282
|
<SplitSquareHorizontal size={iconSize} />
|
|
@@ -261,18 +284,18 @@
|
|
|
261
284
|
<span class="hce-table-btn-tooltip">셀 분할</span>
|
|
262
285
|
</div>
|
|
263
286
|
|
|
264
|
-
<div class="w-px h-5 bg-
|
|
287
|
+
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
265
288
|
|
|
266
289
|
<!-- Line height -->
|
|
267
290
|
<div class="relative">
|
|
268
|
-
<div class="hce-table-btn-wrap">
|
|
291
|
+
<div class="hce-table-btn-wrap" class:hce-tooltip-off={showLineHeight}>
|
|
269
292
|
<button
|
|
270
293
|
type="button"
|
|
271
294
|
onclick={() => {
|
|
272
295
|
showLineHeight = !showLineHeight;
|
|
273
296
|
showColors = false;
|
|
274
297
|
}}
|
|
275
|
-
class="p-1.5 rounded-full transition-colors text-
|
|
298
|
+
class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
276
299
|
>
|
|
277
300
|
<UnfoldVertical size={iconSize} />
|
|
278
301
|
</button>
|
|
@@ -280,13 +303,13 @@
|
|
|
280
303
|
</div>
|
|
281
304
|
{#if showLineHeight}
|
|
282
305
|
<div
|
|
283
|
-
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
|
306
|
+
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-popover border border-border rounded-lg shadow-xl py-1"
|
|
284
307
|
style="min-width: 100px"
|
|
285
308
|
>
|
|
286
309
|
{#each LINE_HEIGHTS as lh}
|
|
287
310
|
<button
|
|
288
311
|
type="button"
|
|
289
|
-
class="w-full text-left px-3 py-1.5 text-xs text-
|
|
312
|
+
class="w-full text-left px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
290
313
|
onclick={() => {
|
|
291
314
|
const { tr } = editor.state;
|
|
292
315
|
const { tableNode, tablePos } = findTableNode();
|
|
@@ -325,7 +348,7 @@
|
|
|
325
348
|
<div class="hce-table-btn-wrap">
|
|
326
349
|
<button
|
|
327
350
|
type="button"
|
|
328
|
-
class="p-1.5 rounded-full transition-colors text-
|
|
351
|
+
class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
329
352
|
onclick={() => {
|
|
330
353
|
const { tableNode, tablePos } = findTableNode();
|
|
331
354
|
if (!tableNode || tablePos < 0) return;
|
|
@@ -369,14 +392,14 @@
|
|
|
369
392
|
|
|
370
393
|
<!-- Background color -->
|
|
371
394
|
<div class="relative">
|
|
372
|
-
<div class="hce-table-btn-wrap">
|
|
395
|
+
<div class="hce-table-btn-wrap" class:hce-tooltip-off={showColors}>
|
|
373
396
|
<button
|
|
374
397
|
type="button"
|
|
375
398
|
onclick={() => {
|
|
376
399
|
showColors = !showColors;
|
|
377
400
|
showLineHeight = false;
|
|
378
401
|
}}
|
|
379
|
-
class="p-1.5 rounded-full transition-colors text-
|
|
402
|
+
class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
380
403
|
>
|
|
381
404
|
<Paintbrush size={iconSize} />
|
|
382
405
|
</button>
|
|
@@ -384,11 +407,11 @@
|
|
|
384
407
|
</div>
|
|
385
408
|
{#if showColors}
|
|
386
409
|
<div
|
|
387
|
-
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
|
410
|
+
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-popover border border-border rounded-lg shadow-xl p-2"
|
|
388
411
|
style="min-width: 160px"
|
|
389
412
|
>
|
|
390
413
|
<p
|
|
391
|
-
class="text-xs font-medium text-
|
|
414
|
+
class="text-xs font-medium text-muted-foreground mb-1.5 px-1"
|
|
392
415
|
>
|
|
393
416
|
배경색
|
|
394
417
|
</p>
|
|
@@ -397,7 +420,7 @@
|
|
|
397
420
|
<button
|
|
398
421
|
type="button"
|
|
399
422
|
title={c.label}
|
|
400
|
-
class="w-full h-7 rounded-md border border-
|
|
423
|
+
class="w-full h-7 rounded-md border border-border transition-transform hover:scale-110"
|
|
401
424
|
style="background: {c.value ||
|
|
402
425
|
'transparent'}; {!c.value
|
|
403
426
|
? 'background-image: linear-gradient(135deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)'
|
|
@@ -417,18 +440,18 @@
|
|
|
417
440
|
{/if}
|
|
418
441
|
</div>
|
|
419
442
|
|
|
420
|
-
<div class="w-px h-5 bg-
|
|
443
|
+
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
421
444
|
|
|
422
445
|
<!-- Delete buttons -->
|
|
423
446
|
<div class="hce-table-btn-wrap">
|
|
424
447
|
<button
|
|
425
448
|
type="button"
|
|
426
449
|
onclick={() => editor.chain().focus().deleteRow().run()}
|
|
427
|
-
disabled={!
|
|
450
|
+
disabled={!canDo((c) => c.deleteRow())}
|
|
428
451
|
class={cn(
|
|
429
452
|
"p-1.5 rounded-full transition-colors",
|
|
430
|
-
"text-
|
|
431
|
-
!
|
|
453
|
+
"text-muted-foreground hover:text-red-300 hover:bg-muted",
|
|
454
|
+
!canDo((c) => c.deleteRow()) && "opacity-30 pointer-events-none",
|
|
432
455
|
)}
|
|
433
456
|
>
|
|
434
457
|
<RowsIcon size={iconSize} />
|
|
@@ -440,11 +463,11 @@
|
|
|
440
463
|
<button
|
|
441
464
|
type="button"
|
|
442
465
|
onclick={() => editor.chain().focus().deleteColumn().run()}
|
|
443
|
-
disabled={!
|
|
466
|
+
disabled={!canDo((c) => c.deleteColumn())}
|
|
444
467
|
class={cn(
|
|
445
468
|
"p-1.5 rounded-full transition-colors",
|
|
446
|
-
"text-
|
|
447
|
-
!
|
|
469
|
+
"text-muted-foreground hover:text-red-300 hover:bg-muted",
|
|
470
|
+
!canDo((c) => c.deleteColumn()) && "opacity-30 pointer-events-none",
|
|
448
471
|
)}
|
|
449
472
|
>
|
|
450
473
|
<ColumnsIcon size={iconSize} />
|
|
@@ -456,11 +479,11 @@
|
|
|
456
479
|
<button
|
|
457
480
|
type="button"
|
|
458
481
|
onclick={() => editor.chain().focus().deleteTable().run()}
|
|
459
|
-
disabled={!
|
|
482
|
+
disabled={!canDo((c) => c.deleteTable())}
|
|
460
483
|
class={cn(
|
|
461
484
|
"p-1.5 rounded-full transition-colors",
|
|
462
|
-
"text-
|
|
463
|
-
!
|
|
485
|
+
"text-muted-foreground hover:text-red-300 hover:bg-muted",
|
|
486
|
+
!canDo((c) => c.deleteTable()) && "opacity-30 pointer-events-none",
|
|
464
487
|
)}
|
|
465
488
|
>
|
|
466
489
|
<Trash2 size={iconSize} />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableBubbleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TableBubbleMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"TableBubbleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TableBubbleMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAqY7C,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -4,24 +4,7 @@
|
|
|
4
4
|
import { TableCell } from "@tiptap/extension-table-cell";
|
|
5
5
|
import { Extension } from "@tiptap/core";
|
|
6
6
|
import { TextSelection } from "@tiptap/pm/state";
|
|
7
|
-
import {
|
|
8
|
-
import cpp from "highlight.js/lib/languages/cpp";
|
|
9
|
-
import python from "highlight.js/lib/languages/python";
|
|
10
|
-
|
|
11
|
-
/*
|
|
12
|
-
* 문법 하이라이터는 **모듈에 한 벌만** 둔다.
|
|
13
|
-
*
|
|
14
|
-
* `common` 은 언어 37종이고, 예전엔 이 등록이 인스턴스 스크립트에 있어 에디터를 세울
|
|
15
|
-
* 때마다 통째로 다시 돌았다. 정올 기출 퀴즈처럼 한 화면에 에디터가 26~27개 서는
|
|
16
|
-
* 곳에서는 그게 1000번 가까이 반복된다.
|
|
17
|
-
*
|
|
18
|
-
* lowlight 는 문법을 담아 두는 레지스트리일 뿐 편집기별 상태를 갖지 않으므로 공유해도
|
|
19
|
-
* 안전하다. (측정해 보면 이 항목 자체는 27개 기준 13ms 수준이라 큰 몫은 아니다 —
|
|
20
|
-
* 다만 공짜로 없앨 수 있는 반복이라 남겨 둘 이유가 없다.)
|
|
21
|
-
*/
|
|
22
|
-
const lowlight = createLowlight(common);
|
|
23
|
-
lowlight.register("cpp", cpp);
|
|
24
|
-
lowlight.register("python", python);
|
|
7
|
+
import { lowlight } from "../utils/lowlight";
|
|
25
8
|
|
|
26
9
|
/*
|
|
27
10
|
* 읽기 전용용 표 확장도 **한 벌만** 만든다.
|
|
@@ -128,8 +111,13 @@
|
|
|
128
111
|
import Typography from "@tiptap/extension-typography";
|
|
129
112
|
import CharacterCount from "@tiptap/extension-character-count";
|
|
130
113
|
import { TableRow } from "@tiptap/extension-table-row";
|
|
131
|
-
import { DetailsContent
|
|
114
|
+
import { DetailsContent } from "@tiptap/extension-details";
|
|
132
115
|
import { FixedDetails } from "../extensions/FixedDetails";
|
|
116
|
+
import {
|
|
117
|
+
NotionBlockquote,
|
|
118
|
+
LeveledDetailsSummary,
|
|
119
|
+
NotionToggleInputRule,
|
|
120
|
+
} from "../extensions/NotionInputRules";
|
|
133
121
|
import FileHandler from "@tiptap/extension-file-handler";
|
|
134
122
|
import type { AnyExtension } from "@tiptap/core";
|
|
135
123
|
import { PdfBlock } from "../extensions/PdfBlock";
|
|
@@ -139,6 +127,7 @@
|
|
|
139
127
|
import { Indent } from "../extensions/Indent";
|
|
140
128
|
import { FileAttachment } from "../extensions/FileAttachment";
|
|
141
129
|
import { MbusVideo } from "../extensions/MbusVideo";
|
|
130
|
+
import { VideoEmbed } from "../extensions/VideoEmbed";
|
|
142
131
|
import { CardBlock } from "../extensions/CardBlock";
|
|
143
132
|
import { MathInline, MathDisplay, type MathPrompt } from "../extensions/Math";
|
|
144
133
|
import FixedToolbar from "./FixedToolbar.svelte";
|
|
@@ -146,6 +135,8 @@
|
|
|
146
135
|
import SlashCommandMenu from "./SlashCommandMenu.svelte";
|
|
147
136
|
import TableBubbleMenu from "./TableBubbleMenu.svelte";
|
|
148
137
|
import MathModal from "./MathModal.svelte";
|
|
138
|
+
import MediaPickerModal from "./MediaPickerModal.svelte";
|
|
139
|
+
import InputModal from "./InputModal.svelte";
|
|
149
140
|
import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
|
|
150
141
|
import { resolveFeatures } from "../types";
|
|
151
142
|
import type { FileResolver } from "../extensions/FileAttachment";
|
|
@@ -160,6 +151,7 @@
|
|
|
160
151
|
onPromptLink,
|
|
161
152
|
onPromptImage,
|
|
162
153
|
onPromptMbus,
|
|
154
|
+
onPromptVideo,
|
|
163
155
|
onPromptCardBackground,
|
|
164
156
|
onPromptMath,
|
|
165
157
|
toolbarEnd,
|
|
@@ -177,6 +169,8 @@
|
|
|
177
169
|
onPromptLink?: PromptHandler;
|
|
178
170
|
onPromptImage?: PromptHandler;
|
|
179
171
|
onPromptMbus?: PromptHandler;
|
|
172
|
+
/** 영상(유튜브·Vimeo 등) URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
|
|
173
|
+
onPromptVideo?: PromptHandler;
|
|
180
174
|
/** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
|
|
181
175
|
onPromptCardBackground?: PromptHandler;
|
|
182
176
|
/** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
|
|
@@ -238,6 +232,93 @@
|
|
|
238
232
|
let uploading = $state(false);
|
|
239
233
|
let pdfInputEl: HTMLInputElement | undefined = $state();
|
|
240
234
|
let fileInputEl: HTMLInputElement | undefined = $state();
|
|
235
|
+
|
|
236
|
+
/*
|
|
237
|
+
* ── 이미지·파일 고르기 모달 ──────────────────────────────────────────────
|
|
238
|
+
* **에디터가 갖는다.** 툴바에서도 슬래시 메뉴에서도 같은 것이 열려야 하고, 실제로 넣는 데
|
|
239
|
+
* 필요한 것(`onUploadFile`, 노드 만드는 법)이 전부 여기 있기 때문이다. 툴바에 두면
|
|
240
|
+
* 슬래시 메뉴가 자기 것을 또 만들어야 한다.
|
|
241
|
+
*
|
|
242
|
+
* ⚠️ **업로드 함수를 준 호스트에게만 연다.** 못 올리는 곳에서 `업로드` 탭을 보여 주면
|
|
243
|
+
* 눌러도 아무 일이 없다. 그런 곳은 예전처럼 URL 입력만 받는다(아래 `pickImage`).
|
|
244
|
+
*/
|
|
245
|
+
let mediaPicker = $state<"image" | "file" | null>(null);
|
|
246
|
+
|
|
247
|
+
/** URL 에서 파일 이름을 뽑는다. 링크로 첨부할 때 표시할 이름이 필요하다. */
|
|
248
|
+
function fileNameFromUrl(raw: string): string {
|
|
249
|
+
try {
|
|
250
|
+
const path = new URL(raw, "http://x").pathname;
|
|
251
|
+
const last = decodeURIComponent(path.split("/").filter(Boolean).pop() || "");
|
|
252
|
+
return last || "파일";
|
|
253
|
+
} catch {
|
|
254
|
+
return "파일";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function pickImage() {
|
|
259
|
+
if (onUploadFile) {
|
|
260
|
+
mediaPicker = "image";
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// 못 올리는 호스트 — 예전 경로(호스트 프롬프트 → 없으면 내장 입력창) 그대로.
|
|
264
|
+
if (onPromptImage) {
|
|
265
|
+
onPromptImage("").then((url) => {
|
|
266
|
+
if (url) editor?.chain().focus().setImage({ src: url }).run();
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
imageUrlPrompt = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function pickFile() {
|
|
274
|
+
if (!onUploadFile) return;
|
|
275
|
+
mediaPicker = "file";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 업로드 없이 URL 만 받던 시절의 내장 입력창. 위 폴백에서만 쓴다. */
|
|
279
|
+
let imageUrlPrompt = $state(false);
|
|
280
|
+
|
|
281
|
+
/*
|
|
282
|
+
* 모달이 돌려주는 네 가지 결말. 모달을 닫는 것도 여기서 한다 —
|
|
283
|
+
* 모달은 "무엇을 골랐는지"만 알리고 그 뒤는 모른다.
|
|
284
|
+
*
|
|
285
|
+
* ⚠️ 업로드는 **실패해도 모달을 닫는다.** 열어 둔 채로 두면 `업로드 중...` 오버레이가
|
|
286
|
+
* 모달 뒤에 깔려 무슨 일이 일어나는지 안 보인다. 실패는 아래 `catch` 가 알린다.
|
|
287
|
+
*/
|
|
288
|
+
function insertImageFile(file: File) {
|
|
289
|
+
mediaPicker = null;
|
|
290
|
+
if (!editor || !onUploadFile) return;
|
|
291
|
+
uploading = true;
|
|
292
|
+
onUploadFile(file)
|
|
293
|
+
.then((url) => {
|
|
294
|
+
editor!.chain().focus().setImage({ src: url }).run();
|
|
295
|
+
})
|
|
296
|
+
.catch(() => {
|
|
297
|
+
alert("이미지 업로드에 실패했습니다.");
|
|
298
|
+
})
|
|
299
|
+
.finally(() => {
|
|
300
|
+
uploading = false;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function insertImageUrl(url: string) {
|
|
305
|
+
mediaPicker = null;
|
|
306
|
+
editor?.chain().focus().setImage({ src: url }).run();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function insertFileUpload(file: File) {
|
|
310
|
+
mediaPicker = null;
|
|
311
|
+
uploadFile(file);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function insertFileUrl(url: string) {
|
|
315
|
+
mediaPicker = null;
|
|
316
|
+
editor
|
|
317
|
+
?.chain()
|
|
318
|
+
.focus()
|
|
319
|
+
.setFileAttachment({ src: url, name: fileNameFromUrl(url) })
|
|
320
|
+
.run();
|
|
321
|
+
}
|
|
241
322
|
let lastEmittedHtml = content; // onChange로 내보낸 마지막 HTML (외부→내부 변경만 감지용)
|
|
242
323
|
let tableObserver: MutationObserver | undefined;
|
|
243
324
|
|
|
@@ -413,10 +494,38 @@
|
|
|
413
494
|
editor = new Editor({
|
|
414
495
|
element: editorElement,
|
|
415
496
|
extensions: [
|
|
497
|
+
/*
|
|
498
|
+
* `link`·`underline` 을 끄는 것은 **v3 이전의 뒷정리**다.
|
|
499
|
+
*
|
|
500
|
+
* ⚠️ StarterKit v2 에는 이 둘이 없어서 아래에 `Link`·`Underline` 을 따로 달았다.
|
|
501
|
+
* v3(3.22.x)부터 StarterKit 이 그것을 품으면서 같은 이름이 두 번 등록되고
|
|
502
|
+
* `Duplicate extension names found: ['link', 'underline']` 경고가 뜬다.
|
|
503
|
+
*
|
|
504
|
+
* 지금은 나중에 등록한 아래 `Link.configure` 가 이긴다 — 실측으로 `target=_blank`
|
|
505
|
+
* 와 `rel=noopener noreferrer` 가 붙는 것을 확인했다. 하지만 **그 우선순위는 계약이
|
|
506
|
+
* 아니다.** TipTap 의 해결 순서가 바뀌면 `openOnClick: false` 와 rel/target 이
|
|
507
|
+
* 조용히 빠진다. 읽기 전용 인스턴스가 한 화면에 26~27 개 서는 곳도 있어서(아래
|
|
508
|
+
* 안내문 주석 참고) 중복 등록과 경고가 그만큼 쌓인다.
|
|
509
|
+
*
|
|
510
|
+
* `codeBlock: false` 와 같은 꼴 — **StarterKit 에서는 끄고 자기 것을 단다** — 로 맞춘다.
|
|
511
|
+
*/
|
|
512
|
+
/*
|
|
513
|
+
* `blockquote: false` 인 이유는 위 셋과 같다 — **끄고 자기 것을 단다.**
|
|
514
|
+
* StarterKit 의 인용문은 `> ` 로 만들어지는데, 노션에 맞춰 `> ` 는 토글에 내주고
|
|
515
|
+
* 인용문은 `" ` 로 옮겼다(`NotionBlockquote`). 입력 규칙은 `configure` 로 바꿀 수
|
|
516
|
+
* 없어서 확장을 갈아 끼우는 것 말고는 방법이 없다.
|
|
517
|
+
*
|
|
518
|
+
* ⚠️ 두 규칙이 같은 `> ` 를 노리게 두면 안 된다. 어느 쪽이 이길지는 확장 등록
|
|
519
|
+
* 순서에 달리게 되어, 같은 키를 쳤는데 인용문이 나올 때와 토글이 나올 때가 갈린다.
|
|
520
|
+
*/
|
|
416
521
|
StarterKit.configure({
|
|
417
522
|
heading: { levels: [1, 2, 3] },
|
|
418
523
|
codeBlock: false,
|
|
524
|
+
blockquote: false,
|
|
525
|
+
link: false,
|
|
526
|
+
underline: false,
|
|
419
527
|
}),
|
|
528
|
+
NotionBlockquote,
|
|
420
529
|
...(extraExtensions.some((ext) => (ext as any).name === 'codeBlock')
|
|
421
530
|
? []
|
|
422
531
|
: [CodeBlockLowlight.configure({
|
|
@@ -483,6 +592,7 @@
|
|
|
483
592
|
...(fileDownloadBaseUrl ? { downloadBaseUrl: fileDownloadBaseUrl } : {}),
|
|
484
593
|
}),
|
|
485
594
|
MbusVideo,
|
|
595
|
+
VideoEmbed,
|
|
486
596
|
CardBlock.configure({ promptBackground: onPromptCardBackground ?? null }),
|
|
487
597
|
/*
|
|
488
598
|
* ⚠️ **수학은 여기서 등록한다.** 예전엔 `MathInline`/`MathDisplay` 를 내보내기만 하고
|
|
@@ -505,7 +615,9 @@
|
|
|
505
615
|
Indent,
|
|
506
616
|
FixedDetails,
|
|
507
617
|
DetailsContent,
|
|
508
|
-
|
|
618
|
+
/* 토글 제목(`# > `)을 위해 `level` 을 붙인 판본. 옛 문서는 level 0 으로 읽힌다. */
|
|
619
|
+
LeveledDetailsSummary,
|
|
620
|
+
NotionToggleInputRule,
|
|
509
621
|
...extraExtensions,
|
|
510
622
|
...(onUploadFile
|
|
511
623
|
? [
|
|
@@ -685,11 +797,12 @@
|
|
|
685
797
|
{editor}
|
|
686
798
|
{features}
|
|
687
799
|
{onPromptLink}
|
|
688
|
-
{onPromptImage}
|
|
689
800
|
{onPromptMbus}
|
|
801
|
+
{onPromptVideo}
|
|
690
802
|
{toolbarEnd}
|
|
691
803
|
onPdfClick={() => pdfInputEl?.click()}
|
|
692
|
-
|
|
804
|
+
onImageClick={pickImage}
|
|
805
|
+
onFileClick={onUploadFile ? pickFile : undefined}
|
|
693
806
|
/>
|
|
694
807
|
{/if}
|
|
695
808
|
|
|
@@ -728,15 +841,16 @@
|
|
|
728
841
|
{editor}
|
|
729
842
|
{features}
|
|
730
843
|
{onPromptLink}
|
|
731
|
-
{onPromptImage}
|
|
732
844
|
{onPromptMbus}
|
|
845
|
+
{onPromptVideo}
|
|
733
846
|
query={slashQuery}
|
|
734
847
|
onClose={closeSlashMenu}
|
|
848
|
+
onImagePick={pickImage}
|
|
735
849
|
onPdfUpload={onUploadFile && features.has('pdf')
|
|
736
850
|
? () => pdfInputEl?.click()
|
|
737
851
|
: undefined}
|
|
738
852
|
onFileUpload={onUploadFile && features.has('file')
|
|
739
|
-
?
|
|
853
|
+
? pickFile
|
|
740
854
|
: undefined}
|
|
741
855
|
/>
|
|
742
856
|
</div>
|
|
@@ -747,6 +861,48 @@
|
|
|
747
861
|
`features` 로 막지 않는다 — `$…$` 입력 규칙과 붙여넣기 변환은 feature 와 무관하게
|
|
748
862
|
항상 살아 있어서, 그렇게 만든 수식을 고치려면 이 모달이 필요하다.
|
|
749
863
|
-->
|
|
864
|
+
<!--
|
|
865
|
+
이미지·파일 고르기. 둘이 **같은 모달**을 쓰고 문구만 다르다
|
|
866
|
+
(`MediaPickerModal` 주석 참고).
|
|
867
|
+
-->
|
|
868
|
+
{#if mediaPicker === 'image'}
|
|
869
|
+
<MediaPickerModal
|
|
870
|
+
title="이미지 추가"
|
|
871
|
+
accept="image/*"
|
|
872
|
+
uploadLabel="파일 업로드"
|
|
873
|
+
linkPlaceholder="이미지 링크 붙여넣기"
|
|
874
|
+
linkConfirmLabel="이미지 임베드"
|
|
875
|
+
linkHint="웹에 있는 모든 이미지와 호환됨"
|
|
876
|
+
onUpload={insertImageFile}
|
|
877
|
+
onLink={insertImageUrl}
|
|
878
|
+
onCancel={() => (mediaPicker = null)}
|
|
879
|
+
/>
|
|
880
|
+
{/if}
|
|
881
|
+
{#if mediaPicker === 'file'}
|
|
882
|
+
<MediaPickerModal
|
|
883
|
+
title="파일 추가"
|
|
884
|
+
uploadLabel="파일을 선택하세요"
|
|
885
|
+
linkPlaceholder="파일 링크 붙여넣기"
|
|
886
|
+
linkConfirmLabel="파일 임베드"
|
|
887
|
+
onUpload={insertFileUpload}
|
|
888
|
+
onLink={insertFileUrl}
|
|
889
|
+
onCancel={() => (mediaPicker = null)}
|
|
890
|
+
/>
|
|
891
|
+
{/if}
|
|
892
|
+
|
|
893
|
+
<!-- 업로드를 못 하는 호스트용 폴백(URL 만 받는다). 위 `pickImage` 참고. -->
|
|
894
|
+
{#if imageUrlPrompt}
|
|
895
|
+
<InputModal
|
|
896
|
+
title="이미지 URL 입력"
|
|
897
|
+
placeholder="https://example.com/image.png"
|
|
898
|
+
onConfirm={(url) => {
|
|
899
|
+
imageUrlPrompt = false;
|
|
900
|
+
editor?.chain().focus().setImage({ src: url }).run();
|
|
901
|
+
}}
|
|
902
|
+
onCancel={() => (imageUrlPrompt = false)}
|
|
903
|
+
/>
|
|
904
|
+
{/if}
|
|
905
|
+
|
|
750
906
|
{#if mathPrompt}
|
|
751
907
|
<MathModal
|
|
752
908
|
latex={mathPrompt.latex}
|
|
@@ -13,6 +13,8 @@ type $$ComponentProps = {
|
|
|
13
13
|
onPromptLink?: PromptHandler;
|
|
14
14
|
onPromptImage?: PromptHandler;
|
|
15
15
|
onPromptMbus?: PromptHandler;
|
|
16
|
+
/** 영상(유튜브·Vimeo 등) URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
|
|
17
|
+
onPromptVideo?: PromptHandler;
|
|
16
18
|
/** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
|
|
17
19
|
onPromptCardBackground?: PromptHandler;
|
|
18
20
|
/** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA8FA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA4BtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAUjD,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQ9E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uDAAuD;IACvD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAmvBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|