@teriusu/rich-editor 0.4.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/README.md +178 -0
- package/dist/components/BubbleToolbar.svelte +299 -0
- package/dist/components/BubbleToolbar.svelte.d.ts +9 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -0
- package/dist/components/FixedToolbar.svelte +892 -0
- package/dist/components/FixedToolbar.svelte.d.ts +11 -0
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -0
- package/dist/components/InputModal.svelte +102 -0
- package/dist/components/InputModal.svelte.d.ts +11 -0
- package/dist/components/InputModal.svelte.d.ts.map +1 -0
- package/dist/components/PdfViewer.svelte +152 -0
- package/dist/components/PdfViewer.svelte.d.ts +8 -0
- package/dist/components/PdfViewer.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +315 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts +14 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableBubbleMenu.svelte +472 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts +8 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -0
- package/dist/components/TipTapEditor.svelte +649 -0
- package/dist/components/TipTapEditor.svelte.d.ts +17 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -0
- package/dist/extensions/Column.d.ts +3 -0
- package/dist/extensions/Column.d.ts.map +1 -0
- package/dist/extensions/Column.js +20 -0
- package/dist/extensions/Columns.d.ts +11 -0
- package/dist/extensions/Columns.d.ts.map +1 -0
- package/dist/extensions/Columns.js +103 -0
- package/dist/extensions/Columns.test.d.ts +2 -0
- package/dist/extensions/Columns.test.d.ts.map +1 -0
- package/dist/extensions/Columns.test.js +62 -0
- package/dist/extensions/FileAttachment.d.ts +24 -0
- package/dist/extensions/FileAttachment.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.js +190 -0
- package/dist/extensions/FileAttachment.test.d.ts +2 -0
- package/dist/extensions/FileAttachment.test.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.test.js +63 -0
- package/dist/extensions/FixedDetails.d.ts +2 -0
- package/dist/extensions/FixedDetails.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.js +100 -0
- package/dist/extensions/FixedDetails.test.d.ts +2 -0
- package/dist/extensions/FixedDetails.test.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.test.js +40 -0
- package/dist/extensions/Indent.d.ts +11 -0
- package/dist/extensions/Indent.d.ts.map +1 -0
- package/dist/extensions/Indent.js +87 -0
- package/dist/extensions/Indent.test.d.ts +2 -0
- package/dist/extensions/Indent.test.d.ts.map +1 -0
- package/dist/extensions/Indent.test.js +78 -0
- package/dist/extensions/PdfBlock.d.ts +3 -0
- package/dist/extensions/PdfBlock.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.js +301 -0
- package/dist/extensions/PdfBlock.test.d.ts +2 -0
- package/dist/extensions/PdfBlock.test.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.test.js +51 -0
- package/dist/extensions/VideoBlock.d.ts +16 -0
- package/dist/extensions/VideoBlock.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.js +87 -0
- package/dist/extensions/VideoBlock.test.d.ts +2 -0
- package/dist/extensions/VideoBlock.test.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.test.js +54 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/styles/editor.css +698 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/cn.d.ts +3 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/cn.test.d.ts +2 -0
- package/dist/utils/cn.test.d.ts.map +1 -0
- package/dist/utils/cn.test.js +28 -0
- package/dist/utils/escape-html.d.ts +2 -0
- package/dist/utils/escape-html.d.ts.map +1 -0
- package/dist/utils/escape-html.js +7 -0
- package/dist/utils/escape-html.test.d.ts +2 -0
- package/dist/utils/escape-html.test.d.ts.map +1 -0
- package/dist/utils/escape-html.test.js +25 -0
- package/dist/utils/pdf.d.ts +9 -0
- package/dist/utils/pdf.d.ts.map +1 -0
- package/dist/utils/pdf.js +33 -0
- package/dist/utils/sanitize.d.ts +9 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +115 -0
- package/dist/utils/sanitize.test.d.ts +2 -0
- package/dist/utils/sanitize.test.d.ts.map +1 -0
- package/dist/utils/sanitize.test.js +206 -0
- package/package.json +76 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
onPdfClick: () => void;
|
|
5
|
+
onFileClick?: () => void;
|
|
6
|
+
onVideoClick?: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const FixedToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type FixedToolbar = ReturnType<typeof FixedToolbar>;
|
|
10
|
+
export default FixedToolbar;
|
|
11
|
+
//# sourceMappingURL=FixedToolbar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA6C1C,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAiiBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from "lucide-svelte";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
title,
|
|
6
|
+
placeholder = "",
|
|
7
|
+
defaultValue = "",
|
|
8
|
+
onConfirm,
|
|
9
|
+
onCancel,
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
defaultValue?: string;
|
|
14
|
+
onConfirm: (value: string) => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
} = $props();
|
|
17
|
+
|
|
18
|
+
let value = $state(defaultValue);
|
|
19
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
20
|
+
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (inputEl) {
|
|
23
|
+
inputEl.focus();
|
|
24
|
+
inputEl.select();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Window-level Escape key handler
|
|
29
|
+
$effect(() => {
|
|
30
|
+
function onKeydown(e: KeyboardEvent) {
|
|
31
|
+
if (e.key === "Escape") {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
onCancel();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
window.addEventListener("keydown", onKeydown);
|
|
37
|
+
return () => window.removeEventListener("keydown", onKeydown);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function handleSubmit() {
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (trimmed) onConfirm(trimmed);
|
|
43
|
+
else onCancel();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleBackdropClick() {
|
|
47
|
+
onCancel();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
51
|
+
if (e.key === "Enter") handleSubmit();
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
56
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
57
|
+
<div
|
|
58
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
|
59
|
+
onclick={handleBackdropClick}
|
|
60
|
+
>
|
|
61
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
62
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
63
|
+
<div
|
|
64
|
+
class="bg-background border border-border rounded-lg shadow-lg p-4 w-80"
|
|
65
|
+
onclick={(e) => e.stopPropagation()}
|
|
66
|
+
>
|
|
67
|
+
<div class="flex items-center justify-between mb-3">
|
|
68
|
+
<span class="text-sm font-medium">{title}</span>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
class="p-0.5 rounded hover:bg-muted text-muted-foreground"
|
|
72
|
+
onclick={onCancel}
|
|
73
|
+
>
|
|
74
|
+
<X size={14} />
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
<input
|
|
78
|
+
bind:this={inputEl}
|
|
79
|
+
type="text"
|
|
80
|
+
class="w-full border border-border rounded px-3 py-1.5 text-sm bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
|
81
|
+
{placeholder}
|
|
82
|
+
bind:value
|
|
83
|
+
onkeydown={handleKeydown}
|
|
84
|
+
/>
|
|
85
|
+
<div class="flex justify-end gap-2 mt-3">
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="px-3 py-1 text-sm rounded border border-border hover:bg-muted"
|
|
89
|
+
onclick={onCancel}
|
|
90
|
+
>
|
|
91
|
+
취소
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="px-3 py-1 text-sm rounded bg-primary text-primary-foreground hover:opacity-90"
|
|
96
|
+
onclick={handleSubmit}
|
|
97
|
+
>
|
|
98
|
+
확인
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
title: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
defaultValue?: string;
|
|
5
|
+
onConfirm: (value: string) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const InputModal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type InputModal = ReturnType<typeof InputModal>;
|
|
10
|
+
export default InputModal;
|
|
11
|
+
//# sourceMappingURL=InputModal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InputModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/InputModal.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AA2EJ,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-svelte";
|
|
4
|
+
import { getPdfJs } from "../utils/pdf";
|
|
5
|
+
|
|
6
|
+
let { src, fileName }: { src: string; fileName?: string } = $props();
|
|
7
|
+
|
|
8
|
+
let canvasEl: HTMLCanvasElement | undefined = $state();
|
|
9
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
let pdf: any = $state(null);
|
|
12
|
+
let page = $state(1);
|
|
13
|
+
let total = $state(0);
|
|
14
|
+
let loading = $state(true);
|
|
15
|
+
let error = $state(false);
|
|
16
|
+
let rendering = false;
|
|
17
|
+
|
|
18
|
+
async function renderPage() {
|
|
19
|
+
if (!pdf || !canvasEl || !containerEl || rendering) return;
|
|
20
|
+
rendering = true;
|
|
21
|
+
try {
|
|
22
|
+
const pageObj = await pdf.getPage(page);
|
|
23
|
+
const unscaledViewport = pageObj.getViewport({ scale: 1 });
|
|
24
|
+
const availableWidth = containerEl.clientWidth - 32;
|
|
25
|
+
if (availableWidth <= 0) return;
|
|
26
|
+
const scale = availableWidth / unscaledViewport.width;
|
|
27
|
+
const viewport = pageObj.getViewport({ scale });
|
|
28
|
+
const dpr = window.devicePixelRatio || 1;
|
|
29
|
+
canvasEl.width = viewport.width * dpr;
|
|
30
|
+
canvasEl.height = viewport.height * dpr;
|
|
31
|
+
canvasEl.style.width = `${viewport.width}px`;
|
|
32
|
+
canvasEl.style.height = `${viewport.height}px`;
|
|
33
|
+
const ctx = canvasEl.getContext("2d");
|
|
34
|
+
if (!ctx) return;
|
|
35
|
+
ctx.resetTransform();
|
|
36
|
+
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
37
|
+
ctx.scale(dpr, dpr);
|
|
38
|
+
await pageObj.render({ canvasContext: ctx, viewport }).promise;
|
|
39
|
+
} finally {
|
|
40
|
+
rendering = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (pdf && page) renderPage();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
onMount(() => {
|
|
49
|
+
let cancelled = false;
|
|
50
|
+
|
|
51
|
+
(async () => {
|
|
52
|
+
try {
|
|
53
|
+
loading = true;
|
|
54
|
+
error = false;
|
|
55
|
+
const pdfjsLib = await getPdfJs();
|
|
56
|
+
const loadingTask = pdfjsLib.getDocument(src);
|
|
57
|
+
const pdfDoc = await loadingTask.promise;
|
|
58
|
+
if (!cancelled) {
|
|
59
|
+
pdf = pdfDoc;
|
|
60
|
+
total = pdfDoc.numPages;
|
|
61
|
+
loading = false;
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error("[PdfViewer] PDF 로드 실패:", src, e);
|
|
65
|
+
if (!cancelled) {
|
|
66
|
+
error = true;
|
|
67
|
+
loading = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
// ResizeObserver
|
|
73
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
74
|
+
const observer = new ResizeObserver(() => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
timeout = setTimeout(renderPage, 100);
|
|
77
|
+
});
|
|
78
|
+
if (containerEl) observer.observe(containerEl);
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true;
|
|
82
|
+
observer.disconnect();
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
{#if error}
|
|
89
|
+
<div class="border border-border rounded-lg p-4 text-center">
|
|
90
|
+
<p class="text-sm text-muted-foreground">PDF를 불러올 수 없습니다.</p>
|
|
91
|
+
<a
|
|
92
|
+
href={src}
|
|
93
|
+
target="_blank"
|
|
94
|
+
rel="noopener noreferrer"
|
|
95
|
+
class="text-sm text-primary hover:underline"
|
|
96
|
+
>
|
|
97
|
+
직접 다운로드
|
|
98
|
+
</a>
|
|
99
|
+
</div>
|
|
100
|
+
{:else if loading}
|
|
101
|
+
<div class="border border-border rounded-lg p-8 text-center">
|
|
102
|
+
<p class="text-sm text-muted-foreground animate-pulse">PDF 로딩 중...</p>
|
|
103
|
+
</div>
|
|
104
|
+
{:else}
|
|
105
|
+
<div class="border border-border rounded-lg overflow-hidden bg-muted/30 my-4">
|
|
106
|
+
<div
|
|
107
|
+
class="flex items-center justify-between px-4 py-2 border-b border-border bg-background"
|
|
108
|
+
>
|
|
109
|
+
<span class="text-xs text-muted-foreground truncate">
|
|
110
|
+
{fileName || "PDF 문서"}
|
|
111
|
+
</span>
|
|
112
|
+
<span class="text-xs text-muted-foreground tabular-nums">
|
|
113
|
+
{page} / {total}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div
|
|
118
|
+
bind:this={containerEl}
|
|
119
|
+
class="flex items-center justify-center p-4 bg-neutral-100 dark:bg-neutral-900"
|
|
120
|
+
>
|
|
121
|
+
<canvas bind:this={canvasEl} class="shadow-sm"></canvas>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{#if total > 1}
|
|
125
|
+
<div
|
|
126
|
+
class="flex items-center justify-center gap-4 px-4 py-2 border-t border-border bg-background"
|
|
127
|
+
>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onclick={() => (page = Math.max(1, page - 1))}
|
|
131
|
+
disabled={page <= 1}
|
|
132
|
+
class="p-1.5 rounded hover:bg-muted transition-colors disabled:opacity-30"
|
|
133
|
+
title="이전 페이지"
|
|
134
|
+
>
|
|
135
|
+
<ChevronLeft class="w-4 h-4" />
|
|
136
|
+
</button>
|
|
137
|
+
<span class="text-sm tabular-nums">
|
|
138
|
+
{page} / {total}
|
|
139
|
+
</span>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onclick={() => (page = Math.min(total, page + 1))}
|
|
143
|
+
disabled={page >= total}
|
|
144
|
+
class="p-1.5 rounded hover:bg-muted transition-colors disabled:opacity-30"
|
|
145
|
+
title="다음 페이지"
|
|
146
|
+
>
|
|
147
|
+
<ChevronRight class="w-4 h-4" />
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
src: string;
|
|
3
|
+
fileName?: string;
|
|
4
|
+
};
|
|
5
|
+
declare const PdfViewer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type PdfViewer = ReturnType<typeof PdfViewer>;
|
|
7
|
+
export default PdfViewer;
|
|
8
|
+
//# sourceMappingURL=PdfViewer.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PdfViewer.svelte.d.ts","sourceRoot":"","sources":["../../src/components/PdfViewer.svelte.ts"],"names":[],"mappings":"AAOC,KAAK,gBAAgB,GAAI;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAoI7D,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import {
|
|
4
|
+
Heading1,
|
|
5
|
+
Heading2,
|
|
6
|
+
Heading3,
|
|
7
|
+
List,
|
|
8
|
+
ListOrdered,
|
|
9
|
+
CheckSquare,
|
|
10
|
+
Quote,
|
|
11
|
+
Minus,
|
|
12
|
+
Code2,
|
|
13
|
+
ImageIcon,
|
|
14
|
+
LinkIcon,
|
|
15
|
+
Table as TableIcon,
|
|
16
|
+
FileText,
|
|
17
|
+
Video,
|
|
18
|
+
ChevronRight,
|
|
19
|
+
Paperclip,
|
|
20
|
+
Film,
|
|
21
|
+
Columns2,
|
|
22
|
+
Columns3,
|
|
23
|
+
} from "lucide-svelte";
|
|
24
|
+
import type { SlashMenuItem } from "../types";
|
|
25
|
+
import type { Component } from "svelte";
|
|
26
|
+
|
|
27
|
+
const SI = 14;
|
|
28
|
+
|
|
29
|
+
const SLASH_MENU_ITEMS_DATA: {
|
|
30
|
+
label: string;
|
|
31
|
+
keywords: string;
|
|
32
|
+
icon: Component<{ size?: number }>;
|
|
33
|
+
command: (editor: Editor) => void;
|
|
34
|
+
}[] = [
|
|
35
|
+
{
|
|
36
|
+
label: "제목 1",
|
|
37
|
+
keywords: "heading h1 제목",
|
|
38
|
+
icon: Heading1,
|
|
39
|
+
command: (editor) =>
|
|
40
|
+
editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "제목 2",
|
|
44
|
+
keywords: "heading h2 제목",
|
|
45
|
+
icon: Heading2,
|
|
46
|
+
command: (editor) =>
|
|
47
|
+
editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "제목 3",
|
|
51
|
+
keywords: "heading h3 제목",
|
|
52
|
+
icon: Heading3,
|
|
53
|
+
command: (editor) =>
|
|
54
|
+
editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "글머리 목록",
|
|
58
|
+
keywords: "bullet list 목록 리스트",
|
|
59
|
+
icon: List,
|
|
60
|
+
command: (editor) => editor.chain().focus().toggleBulletList().run(),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "번호 목록",
|
|
64
|
+
keywords: "ordered number list 번호 리스트",
|
|
65
|
+
icon: ListOrdered,
|
|
66
|
+
command: (editor) => editor.chain().focus().toggleOrderedList().run(),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "체크리스트",
|
|
70
|
+
keywords: "checklist task todo 체크 할일",
|
|
71
|
+
icon: CheckSquare,
|
|
72
|
+
command: (editor) => editor.chain().focus().toggleTaskList().run(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "인용문",
|
|
76
|
+
keywords: "quote blockquote 인용",
|
|
77
|
+
icon: Quote,
|
|
78
|
+
command: (editor) => editor.chain().focus().toggleBlockquote().run(),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "구분선",
|
|
82
|
+
keywords: "divider hr horizontal rule 구분",
|
|
83
|
+
icon: Minus,
|
|
84
|
+
command: (editor) => editor.chain().focus().setHorizontalRule().run(),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "C++ 코드",
|
|
88
|
+
keywords: "code cpp c++ 코드 블록",
|
|
89
|
+
icon: Code2,
|
|
90
|
+
command: (editor) =>
|
|
91
|
+
editor.chain().focus().setCodeBlock({ language: "cpp" }).run(),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Python 코드",
|
|
95
|
+
keywords: "code python 파이썬 코드 블록",
|
|
96
|
+
icon: Code2,
|
|
97
|
+
command: (editor) =>
|
|
98
|
+
editor.chain().focus().setCodeBlock({ language: "python" }).run(),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "토글",
|
|
102
|
+
keywords: "toggle details 접기 펼치기 토글",
|
|
103
|
+
icon: ChevronRight,
|
|
104
|
+
command: (editor) => editor.chain().focus().setDetails().run(),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: "표",
|
|
108
|
+
keywords: "table 표 테이블",
|
|
109
|
+
icon: TableIcon,
|
|
110
|
+
command: (editor) =>
|
|
111
|
+
editor
|
|
112
|
+
.chain()
|
|
113
|
+
.focus()
|
|
114
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
115
|
+
.run(),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: "2단 컬럼",
|
|
119
|
+
keywords: "column 컬럼 2단 분할",
|
|
120
|
+
icon: Columns2,
|
|
121
|
+
command: (editor) => editor.chain().focus().setColumns(2).run(),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: "3단 컬럼",
|
|
125
|
+
keywords: "column 컬럼 3단 분할",
|
|
126
|
+
icon: Columns3,
|
|
127
|
+
command: (editor) => editor.chain().focus().setColumns(3).run(),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
label: "YouTube 영상",
|
|
131
|
+
keywords: "youtube video 영상 동영상 유튜브",
|
|
132
|
+
icon: Video,
|
|
133
|
+
command: (editor) => {
|
|
134
|
+
const url = window.prompt("YouTube URL을 입력하세요");
|
|
135
|
+
if (url) editor.chain().focus().setYoutubeVideo({ src: url }).run();
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: "이미지",
|
|
140
|
+
keywords: "image 이미지 사진 img",
|
|
141
|
+
icon: ImageIcon,
|
|
142
|
+
command: (editor) => {
|
|
143
|
+
const url = window.prompt("이미지 URL을 입력하세요");
|
|
144
|
+
if (url) editor.chain().focus().setImage({ src: url }).run();
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: "링크",
|
|
149
|
+
keywords: "link url 링크 하이퍼",
|
|
150
|
+
icon: LinkIcon,
|
|
151
|
+
command: (editor) => {
|
|
152
|
+
const url = window.prompt("링크 URL을 입력하세요");
|
|
153
|
+
if (url)
|
|
154
|
+
editor
|
|
155
|
+
.chain()
|
|
156
|
+
.focus()
|
|
157
|
+
.extendMarkRange("link")
|
|
158
|
+
.setLink({ href: url })
|
|
159
|
+
.run();
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
let {
|
|
165
|
+
editor,
|
|
166
|
+
query,
|
|
167
|
+
onClose,
|
|
168
|
+
onPdfUpload,
|
|
169
|
+
onFileUpload,
|
|
170
|
+
onVideoUpload,
|
|
171
|
+
}: {
|
|
172
|
+
editor: Editor;
|
|
173
|
+
query: string;
|
|
174
|
+
onClose: () => void;
|
|
175
|
+
onPdfUpload?: () => void;
|
|
176
|
+
onFileUpload?: () => void;
|
|
177
|
+
onVideoUpload?: () => void;
|
|
178
|
+
} = $props();
|
|
179
|
+
|
|
180
|
+
let selectedIndex = $state(0);
|
|
181
|
+
let menuEl: HTMLDivElement | undefined = $state();
|
|
182
|
+
let mouseMovedSinceKeyboard = $state(true);
|
|
183
|
+
|
|
184
|
+
const allItems = $derived.by(() => {
|
|
185
|
+
const items = [...SLASH_MENU_ITEMS_DATA];
|
|
186
|
+
if (onPdfUpload) {
|
|
187
|
+
items.push({
|
|
188
|
+
label: "PDF 파일",
|
|
189
|
+
keywords: "pdf 파일 문서",
|
|
190
|
+
icon: FileText,
|
|
191
|
+
command: () => onPdfUpload!(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (onVideoUpload) {
|
|
195
|
+
items.push({
|
|
196
|
+
label: "영상 파일",
|
|
197
|
+
keywords: "video 영상 동영상 비디오 메타버스",
|
|
198
|
+
icon: Film,
|
|
199
|
+
command: () => onVideoUpload!(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (onFileUpload) {
|
|
203
|
+
items.push({
|
|
204
|
+
label: "파일 첨부",
|
|
205
|
+
keywords: "file attach 파일 첨부",
|
|
206
|
+
icon: Paperclip,
|
|
207
|
+
command: () => onFileUpload!(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return items;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const filtered = $derived.by(() => {
|
|
214
|
+
const q = query.toLowerCase();
|
|
215
|
+
return q
|
|
216
|
+
? allItems.filter(
|
|
217
|
+
(item) =>
|
|
218
|
+
item.label.toLowerCase().includes(q) ||
|
|
219
|
+
item.keywords.toLowerCase().includes(q),
|
|
220
|
+
)
|
|
221
|
+
: allItems;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
$effect(() => {
|
|
225
|
+
// Reset index when query changes
|
|
226
|
+
query;
|
|
227
|
+
selectedIndex = 0;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
$effect(() => {
|
|
231
|
+
// Scroll selected into view
|
|
232
|
+
if (menuEl) {
|
|
233
|
+
const el = menuEl.querySelector(`[data-index="${selectedIndex}"]`);
|
|
234
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
$effect(() => {
|
|
239
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
240
|
+
if (e.key === "ArrowDown") {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
if (filtered.length === 0) return;
|
|
243
|
+
mouseMovedSinceKeyboard = false;
|
|
244
|
+
selectedIndex = (selectedIndex + 1) % filtered.length;
|
|
245
|
+
} else if (e.key === "ArrowUp") {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
if (filtered.length === 0) return;
|
|
248
|
+
mouseMovedSinceKeyboard = false;
|
|
249
|
+
selectedIndex =
|
|
250
|
+
selectedIndex <= 0 ? filtered.length - 1 : selectedIndex - 1;
|
|
251
|
+
} else if (e.key === "Enter") {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
if (filtered[selectedIndex]) {
|
|
254
|
+
onClose();
|
|
255
|
+
filtered[selectedIndex].command(editor);
|
|
256
|
+
}
|
|
257
|
+
} else if (e.key === "Escape") {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
onClose();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
263
|
+
return () => document.removeEventListener("keydown", handleKeyDown, true);
|
|
264
|
+
});
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
{#if filtered.length === 0}
|
|
268
|
+
<div
|
|
269
|
+
bind:this={menuEl}
|
|
270
|
+
class="z-50 bg-popover border border-border rounded-lg shadow-lg p-2"
|
|
271
|
+
style="width: 180px"
|
|
272
|
+
>
|
|
273
|
+
<p class="text-xs text-muted-foreground px-2 py-1">결과 없음</p>
|
|
274
|
+
</div>
|
|
275
|
+
{:else}
|
|
276
|
+
<div
|
|
277
|
+
bind:this={menuEl}
|
|
278
|
+
class="z-50 bg-popover border border-border rounded-lg shadow-lg overflow-y-auto py-1"
|
|
279
|
+
style="width: 180px; max-height: 200px"
|
|
280
|
+
>
|
|
281
|
+
<p
|
|
282
|
+
class="text-[10px] font-medium text-muted-foreground uppercase tracking-wider px-3 py-1.5"
|
|
283
|
+
>
|
|
284
|
+
블록
|
|
285
|
+
</p>
|
|
286
|
+
{#each filtered as item, i}
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
data-index={i}
|
|
290
|
+
class="w-full flex items-center gap-2 px-2 py-1.5 text-left transition-colors rounded-md mx-0 {i ===
|
|
291
|
+
selectedIndex
|
|
292
|
+
? 'bg-accent text-accent-foreground'
|
|
293
|
+
: 'hover:bg-muted text-foreground'}"
|
|
294
|
+
onmousemove={() => {
|
|
295
|
+
if (!mouseMovedSinceKeyboard) {
|
|
296
|
+
mouseMovedSinceKeyboard = true;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
selectedIndex = i;
|
|
300
|
+
}}
|
|
301
|
+
onclick={() => {
|
|
302
|
+
onClose();
|
|
303
|
+
item.command(editor);
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
<span
|
|
307
|
+
class="flex items-center justify-center w-6 h-6 rounded border border-border bg-background text-muted-foreground shrink-0"
|
|
308
|
+
>
|
|
309
|
+
<item.icon size={SI} />
|
|
310
|
+
</span>
|
|
311
|
+
<span class="text-sm font-medium">{item.label}</span>
|
|
312
|
+
</button>
|
|
313
|
+
{/each}
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
editor: Editor;
|
|
5
|
+
query: string;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onPdfUpload?: () => void;
|
|
8
|
+
onFileUpload?: () => void;
|
|
9
|
+
onVideoUpload?: () => void;
|
|
10
|
+
};
|
|
11
|
+
declare const SlashCommandMenu: Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type SlashCommandMenu = ReturnType<typeof SlashCommandMenu>;
|
|
13
|
+
export default SlashCommandMenu;
|
|
14
|
+
//# sourceMappingURL=SlashCommandMenu.svelte.d.ts.map
|
|
@@ -0,0 +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;AAuB3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,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,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAkRJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|