@teriusu/rich-editor 0.4.1 → 0.5.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.
@@ -21,18 +21,20 @@
21
21
  Columns2,
22
22
  Columns3,
23
23
  } from "lucide-svelte";
24
- import type { SlashMenuItem } from "../types";
24
+ import type { SlashMenuItem, ToolbarFeature } from "../types";
25
25
  import type { Component } from "svelte";
26
26
 
27
27
  const SI = 14;
28
28
 
29
29
  const SLASH_MENU_ITEMS_DATA: {
30
+ feature: ToolbarFeature;
30
31
  label: string;
31
32
  keywords: string;
32
33
  icon: Component<{ size?: number }>;
33
34
  command: (editor: Editor) => void;
34
35
  }[] = [
35
36
  {
37
+ feature: "h1",
36
38
  label: "제목 1",
37
39
  keywords: "heading h1 제목",
38
40
  icon: Heading1,
@@ -40,6 +42,7 @@
40
42
  editor.chain().focus().toggleHeading({ level: 1 }).run(),
41
43
  },
42
44
  {
45
+ feature: "h2",
43
46
  label: "제목 2",
44
47
  keywords: "heading h2 제목",
45
48
  icon: Heading2,
@@ -47,6 +50,7 @@
47
50
  editor.chain().focus().toggleHeading({ level: 2 }).run(),
48
51
  },
49
52
  {
53
+ feature: "h3",
50
54
  label: "제목 3",
51
55
  keywords: "heading h3 제목",
52
56
  icon: Heading3,
@@ -54,36 +58,42 @@
54
58
  editor.chain().focus().toggleHeading({ level: 3 }).run(),
55
59
  },
56
60
  {
61
+ feature: "bullet-list",
57
62
  label: "글머리 목록",
58
63
  keywords: "bullet list 목록 리스트",
59
64
  icon: List,
60
65
  command: (editor) => editor.chain().focus().toggleBulletList().run(),
61
66
  },
62
67
  {
68
+ feature: "ordered-list",
63
69
  label: "번호 목록",
64
70
  keywords: "ordered number list 번호 리스트",
65
71
  icon: ListOrdered,
66
72
  command: (editor) => editor.chain().focus().toggleOrderedList().run(),
67
73
  },
68
74
  {
75
+ feature: "checklist",
69
76
  label: "체크리스트",
70
77
  keywords: "checklist task todo 체크 할일",
71
78
  icon: CheckSquare,
72
79
  command: (editor) => editor.chain().focus().toggleTaskList().run(),
73
80
  },
74
81
  {
82
+ feature: "blockquote",
75
83
  label: "인용문",
76
84
  keywords: "quote blockquote 인용",
77
85
  icon: Quote,
78
86
  command: (editor) => editor.chain().focus().toggleBlockquote().run(),
79
87
  },
80
88
  {
89
+ feature: "horizontal-rule",
81
90
  label: "구분선",
82
91
  keywords: "divider hr horizontal rule 구분",
83
92
  icon: Minus,
84
93
  command: (editor) => editor.chain().focus().setHorizontalRule().run(),
85
94
  },
86
95
  {
96
+ feature: "code-block",
87
97
  label: "C++ 코드",
88
98
  keywords: "code cpp c++ 코드 블록",
89
99
  icon: Code2,
@@ -91,6 +101,7 @@
91
101
  editor.chain().focus().setCodeBlock({ language: "cpp" }).run(),
92
102
  },
93
103
  {
104
+ feature: "code-block",
94
105
  label: "Python 코드",
95
106
  keywords: "code python 파이썬 코드 블록",
96
107
  icon: Code2,
@@ -98,12 +109,14 @@
98
109
  editor.chain().focus().setCodeBlock({ language: "python" }).run(),
99
110
  },
100
111
  {
112
+ feature: "toggle",
101
113
  label: "토글",
102
114
  keywords: "toggle details 접기 펼치기 토글",
103
115
  icon: ChevronRight,
104
116
  command: (editor) => editor.chain().focus().setDetails().run(),
105
117
  },
106
118
  {
119
+ feature: "table",
107
120
  label: "표",
108
121
  keywords: "table 표 테이블",
109
122
  icon: TableIcon,
@@ -115,18 +128,21 @@
115
128
  .run(),
116
129
  },
117
130
  {
131
+ feature: "columns-2",
118
132
  label: "2단 컬럼",
119
133
  keywords: "column 컬럼 2단 분할",
120
134
  icon: Columns2,
121
135
  command: (editor) => editor.chain().focus().setColumns(2).run(),
122
136
  },
123
137
  {
138
+ feature: "columns-3",
124
139
  label: "3단 컬럼",
125
140
  keywords: "column 컬럼 3단 분할",
126
141
  icon: Columns3,
127
142
  command: (editor) => editor.chain().focus().setColumns(3).run(),
128
143
  },
129
144
  {
145
+ feature: "youtube",
130
146
  label: "YouTube 영상",
131
147
  keywords: "youtube video 영상 동영상 유튜브",
132
148
  icon: Video,
@@ -136,6 +152,7 @@
136
152
  },
137
153
  },
138
154
  {
155
+ feature: "image",
139
156
  label: "이미지",
140
157
  keywords: "image 이미지 사진 img",
141
158
  icon: ImageIcon,
@@ -145,6 +162,7 @@
145
162
  },
146
163
  },
147
164
  {
165
+ feature: "link",
148
166
  label: "링크",
149
167
  keywords: "link url 링크 하이퍼",
150
168
  icon: LinkIcon,
@@ -163,6 +181,7 @@
163
181
 
164
182
  let {
165
183
  editor,
184
+ features,
166
185
  query,
167
186
  onClose,
168
187
  onPdfUpload,
@@ -170,6 +189,7 @@
170
189
  onVideoUpload,
171
190
  }: {
172
191
  editor: Editor;
192
+ features: Set<ToolbarFeature>;
173
193
  query: string;
174
194
  onClose: () => void;
175
195
  onPdfUpload?: () => void;
@@ -182,9 +202,10 @@
182
202
  let mouseMovedSinceKeyboard = $state(true);
183
203
 
184
204
  const allItems = $derived.by(() => {
185
- const items = [...SLASH_MENU_ITEMS_DATA];
205
+ const items = SLASH_MENU_ITEMS_DATA.filter((item) => features.has(item.feature));
186
206
  if (onPdfUpload) {
187
207
  items.push({
208
+ feature: "pdf" as ToolbarFeature,
188
209
  label: "PDF 파일",
189
210
  keywords: "pdf 파일 문서",
190
211
  icon: FileText,
@@ -193,6 +214,7 @@
193
214
  }
194
215
  if (onVideoUpload) {
195
216
  items.push({
217
+ feature: "video" as ToolbarFeature,
196
218
  label: "영상 파일",
197
219
  keywords: "video 영상 동영상 비디오 메타버스",
198
220
  icon: Film,
@@ -201,6 +223,7 @@
201
223
  }
202
224
  if (onFileUpload) {
203
225
  items.push({
226
+ feature: "file" as ToolbarFeature,
204
227
  label: "파일 첨부",
205
228
  keywords: "file attach 파일 첨부",
206
229
  icon: Paperclip,
@@ -1,7 +1,9 @@
1
1
  import type { Editor } from "@tiptap/core";
2
+ import type { ToolbarFeature } from "../types";
2
3
  import type { Component } from "svelte";
3
4
  type $$ComponentProps = {
4
5
  editor: Editor;
6
+ features: Set<ToolbarFeature>;
5
7
  query: string;
6
8
  onClose: () => void;
7
9
  onPdfUpload?: () => void;
@@ -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;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"}
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;AAsB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,UAAU,CAAC;AAC9D,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,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAwSJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -41,7 +41,8 @@
41
41
  import BubbleToolbar from "./BubbleToolbar.svelte";
42
42
  import SlashCommandMenu from "./SlashCommandMenu.svelte";
43
43
  import TableBubbleMenu from "./TableBubbleMenu.svelte";
44
- import type { UploadHandler, ToolbarMode } from "../types";
44
+ import type { UploadHandler, ToolbarMode, ToolbarFeature } from "../types";
45
+ import { resolveFeatures } from "../types";
45
46
  import type { FileResolver } from "../extensions/FileAttachment";
46
47
 
47
48
  const cellAttrs = {
@@ -115,6 +116,7 @@
115
116
  extensions: extraExtensions = [],
116
117
  editable = true,
117
118
  toolbar = 'full',
119
+ features: featuresOverride,
118
120
  }: {
119
121
  content: string;
120
122
  onChange: (html: string) => void;
@@ -124,8 +126,11 @@
124
126
  extensions?: AnyExtension[];
125
127
  editable?: boolean;
126
128
  toolbar?: ToolbarMode;
129
+ features?: ToolbarFeature[];
127
130
  } = $props();
128
131
 
132
+ const features = $derived(resolveFeatures(toolbar, featuresOverride));
133
+
129
134
  let editorElement: HTMLDivElement | undefined = $state();
130
135
  let editor: Editor | undefined = $state();
131
136
  let uploading = $state(false);
@@ -224,13 +229,19 @@
224
229
  uploading = true;
225
230
  const size = file.size;
226
231
  onUploadFile(file)
227
- .then((url) => {
232
+ .then((result) => {
233
+ const isFileId = result && !result.includes("/") && !result.includes(":");
228
234
  editor!
229
235
  .chain()
230
236
  .focus()
231
237
  .insertContent({
232
238
  type: "fileAttachment",
233
- attrs: { src: url, name: file.name, size },
239
+ attrs: {
240
+ src: isFileId ? null : result,
241
+ fileId: isFileId ? result : null,
242
+ name: file.name,
243
+ size,
244
+ },
234
245
  })
235
246
  .run();
236
247
  })
@@ -481,7 +492,10 @@
481
492
 
482
493
  // File resolver를 storage에 등록
483
494
  if (onResolveFile) {
484
- editor.storage.fileAttachment = { resolver: onResolveFile };
495
+ editor.storage.fileAttachment = {
496
+ ...editor.storage.fileAttachment,
497
+ resolver: onResolveFile,
498
+ };
485
499
  }
486
500
 
487
501
  editor.on("update", handleUpdate);
@@ -545,13 +559,14 @@
545
559
  </script>
546
560
 
547
561
  <div
548
- class="hce-editor-wrapper relative{editable && toolbar === 'full' ? ' border border-border rounded-xl bg-background' : ''}"
562
+ class="hce-editor-wrapper relative{editable && features.has('fixed-toolbar') ? ' border border-border rounded-xl bg-background' : ''}"
549
563
  ondragover={(e) => e.preventDefault()}
550
564
  ondrop={(e) => { if (!onUploadFile) e.preventDefault(); }}
551
565
  >
552
- {#if editor && editable && toolbar === 'full'}
566
+ {#if editor && editable && features.has('fixed-toolbar')}
553
567
  <FixedToolbar
554
568
  {editor}
569
+ {features}
555
570
  onPdfClick={() => pdfInputEl?.click()}
556
571
  onFileClick={onUploadFile ? () => fileInputEl?.click() : undefined}
557
572
  onVideoClick={onUploadFile ? () => videoInputEl?.click() : undefined}
@@ -561,54 +576,57 @@
561
576
  <div bind:this={editorElement}></div>
562
577
 
563
578
  {#if editor && editable}
564
- <BubbleToolbar {editor} minimal={toolbar === 'minimal'} />
579
+ {#if features.has('bubble-toolbar')}
580
+ <BubbleToolbar {editor} {features} />
581
+ {/if}
565
582
 
566
- {#if toolbar !== 'minimal'}
583
+ {#if features.has('table-menu')}
567
584
  <TableBubbleMenu {editor} />
568
585
  {/if}
569
586
 
570
- {#if toolbar === 'full'}
571
- {#if uploading}
572
- <div
573
- class="absolute inset-0 flex items-center justify-center bg-background/60 rounded-xl"
574
- >
575
- <p class="text-sm text-muted-foreground animate-pulse">
576
- 업로드 중...
577
- </p>
578
- </div>
579
- {/if}
587
+ {#if features.has('upload-overlay') && uploading}
588
+ <div
589
+ class="absolute inset-0 flex items-center justify-center bg-background/60 rounded-xl"
590
+ >
591
+ <p class="text-sm text-muted-foreground animate-pulse">
592
+ 업로드 중...
593
+ </p>
594
+ </div>
580
595
  {/if}
581
596
 
582
- {#if toolbar !== 'minimal' && slashMenuOpen}
597
+ {#if features.has('slash-menu') && slashMenuOpen}
583
598
  <div
584
599
  style="top: {slashMenuPos.top}px; left: {slashMenuPos.left}px"
585
600
  class="fixed z-50"
586
601
  >
587
602
  <SlashCommandMenu
588
603
  {editor}
604
+ {features}
589
605
  query={slashQuery}
590
606
  onClose={closeSlashMenu}
591
- onPdfUpload={onUploadFile
607
+ onPdfUpload={onUploadFile && features.has('pdf')
592
608
  ? () => pdfInputEl?.click()
593
609
  : undefined}
594
- onFileUpload={onUploadFile
610
+ onFileUpload={onUploadFile && features.has('file')
595
611
  ? () => fileInputEl?.click()
596
612
  : undefined}
597
- onVideoUpload={onUploadFile
613
+ onVideoUpload={onUploadFile && features.has('video')
598
614
  ? () => videoInputEl?.click()
599
615
  : undefined}
600
616
  />
601
617
  </div>
602
618
  {/if}
603
619
 
604
- {#if toolbar === 'full'}
620
+ {#if features.has('character-count')}
605
621
  <div
606
622
  class="flex justify-end px-4 py-2 text-xs text-muted-foreground border-t border-border"
607
623
  >
608
624
  {editor.storage.characterCount.characters()} 자 ·
609
625
  {editor.storage.characterCount.words()} 단어
610
626
  </div>
627
+ {/if}
611
628
 
629
+ {#if features.has('pdf') || features.has('file') || features.has('video')}
612
630
  <input
613
631
  bind:this={pdfInputEl}
614
632
  type="file"
@@ -1,5 +1,5 @@
1
1
  import { type AnyExtension } from "@tiptap/core";
2
- import type { UploadHandler, ToolbarMode } from "../types";
2
+ import type { UploadHandler, ToolbarMode, ToolbarFeature } from "../types";
3
3
  import type { FileResolver } from "../extensions/FileAttachment";
4
4
  type $$ComponentProps = {
5
5
  content: string;
@@ -10,6 +10,7 @@ type $$ComponentProps = {
10
10
  extensions?: AnyExtension[];
11
11
  editable?: boolean;
12
12
  toolbar?: ToolbarMode;
13
+ features?: ToolbarFeature[];
13
14
  };
14
15
  declare const TipTapEditor: import("svelte").Component<$$ComponentProps, {}, "">;
15
16
  type TipTapEditor = ReturnType<typeof TipTapEditor>;
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3D,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,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,CAAC;AA4lBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3E,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,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;AA2mBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AA6BD,eAAO,MAAM,cAAc,kCA6KzB,CAAC"}
1
+ {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkCD,eAAO,MAAM,cAAc,kCAmNzB,CAAC"}
@@ -6,6 +6,10 @@ function formatFileSize(bytes) {
6
6
  return `${(bytes / 1024).toFixed(1)} KB`;
7
7
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8
8
  }
9
+ function isInlineable(name) {
10
+ const ext = name.split(".").pop()?.toLowerCase() || "";
11
+ return ["pdf", "png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext);
12
+ }
9
13
  function getFileIcon(name) {
10
14
  const ext = name.split(".").pop()?.toLowerCase() || "";
11
15
  const icons = {
@@ -111,16 +115,47 @@ export const FileAttachment = Node.create({
111
115
  const info = document.createElement("div");
112
116
  info.style.cssText = "flex:1;min-width:0;";
113
117
  const nameEl = document.createElement("a");
114
- nameEl.target = "_blank";
115
- nameEl.rel = "noopener noreferrer";
116
118
  nameEl.textContent = node.attrs.name;
117
119
  nameEl.style.cssText =
118
- "display:block;font-size:13px;font-weight:600;color:var(--primary, #4A7DAC);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
120
+ "display:block;font-size:13px;font-weight:600;color:var(--primary, #4A7DAC);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;";
121
+ let resolvedSrc = null;
122
+ let resolvedName = node.attrs.name;
123
+ function getProxyUrl() {
124
+ const fileId = node.attrs.fileId;
125
+ if (!fileId)
126
+ return null;
127
+ const baseUrl = editor.storage.fileAttachment?.downloadBaseUrl || "/api/upload";
128
+ return `${baseUrl}/${fileId}/download`;
129
+ }
130
+ function handleClick(e) {
131
+ e.preventDefault();
132
+ const proxyUrl = getProxyUrl();
133
+ const targetUrl = proxyUrl || resolvedSrc;
134
+ if (!targetUrl)
135
+ return;
136
+ if (isInlineable(resolvedName)) {
137
+ window.open(targetUrl, "_blank");
138
+ }
139
+ else {
140
+ fetch(targetUrl)
141
+ .then((res) => res.blob())
142
+ .then((blob) => {
143
+ const url = URL.createObjectURL(blob);
144
+ const a = document.createElement("a");
145
+ a.href = url;
146
+ a.download = resolvedName;
147
+ a.click();
148
+ URL.revokeObjectURL(url);
149
+ });
150
+ }
151
+ }
152
+ nameEl.addEventListener("click", handleClick);
119
153
  const sizeEl = document.createElement("span");
120
154
  sizeEl.style.cssText =
121
155
  "font-size:11px;color:var(--muted-foreground, #718096);";
122
156
  // URL이 있으면 바로 표시
123
157
  if (node.attrs.src) {
158
+ resolvedSrc = node.attrs.src;
124
159
  nameEl.href = node.attrs.src;
125
160
  if (node.attrs.size)
126
161
  sizeEl.textContent = formatFileSize(node.attrs.size);
@@ -134,9 +169,11 @@ export const FileAttachment = Node.create({
134
169
  if (resolver) {
135
170
  resolver(node.attrs.fileId)
136
171
  .then((result) => {
172
+ resolvedSrc = result.src;
137
173
  nameEl.href = result.src;
138
174
  nameEl.style.pointerEvents = "";
139
175
  if (result.name) {
176
+ resolvedName = result.name;
140
177
  nameEl.textContent = result.name;
141
178
  icon.textContent = getFileIcon(result.name);
142
179
  }
@@ -178,7 +215,10 @@ export const FileAttachment = Node.create({
178
215
  };
179
216
  },
180
217
  addStorage() {
181
- return { resolver: null };
218
+ return {
219
+ resolver: null,
220
+ downloadBaseUrl: "/api/upload",
221
+ };
182
222
  },
183
223
  addCommands() {
184
224
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAInE,eAAO,MAAM,QAAQ,sBAwTnB,CAAC"}
1
+ {"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAInE,eAAO,MAAM,QAAQ,sBA8TnB,CAAC"}
@@ -245,31 +245,36 @@ export const PdfBlock = TiptapNode.create({
245
245
  }
246
246
  }
247
247
  // URL 해결 후 PDF 로드
248
+ function getProxyUrl(fileId) {
249
+ const baseUrl = editor.storage.fileAttachment?.downloadBaseUrl || "/api/upload";
250
+ return `${baseUrl}/${fileId}/download`;
251
+ }
248
252
  if (node.attrs.src) {
249
- openLink.href = node.attrs.src;
250
- loadPdf(node.attrs.src);
253
+ if (node.attrs.fileId) {
254
+ // fileId 있으면 프록시 URL 사용 (경로 숨김)
255
+ const proxyUrl = getProxyUrl(node.attrs.fileId);
256
+ openLink.href = proxyUrl;
257
+ loadPdf(proxyUrl);
258
+ }
259
+ else {
260
+ openLink.href = node.attrs.src;
261
+ loadPdf(node.attrs.src);
262
+ }
251
263
  }
252
264
  else if (node.attrs.fileId) {
253
- // resolver로 URL 획득
254
- openLink.href = "#";
265
+ const proxyUrl = getProxyUrl(node.attrs.fileId);
266
+ openLink.href = proxyUrl;
267
+ // resolver로 이름 획득
255
268
  const resolver = editor.storage.fileAttachment?.resolver;
256
269
  if (resolver) {
257
270
  resolver(node.attrs.fileId)
258
271
  .then((result) => {
259
- openLink.href = result.src;
260
272
  if (result.name)
261
273
  nameSpan.textContent = result.name;
262
- loadPdf(result.src);
263
274
  })
264
- .catch(() => {
265
- loadingDiv.innerHTML =
266
- '<p class="text-sm text-muted-foreground">PDF URL\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p>';
267
- });
268
- }
269
- else {
270
- loadingDiv.innerHTML =
271
- `<p class="text-sm text-muted-foreground">PDF ID: ${node.attrs.fileId}</p>`;
275
+ .catch(() => { });
272
276
  }
277
+ loadPdf(proxyUrl);
273
278
  }
274
279
  return {
275
280
  dom,
package/dist/index.d.ts CHANGED
@@ -16,5 +16,6 @@ export { Column } from "./extensions/Column";
16
16
  export { sanitizeHtml, stripHtmlToExcerpt, transformLegacyHtml } from "./utils/sanitize";
17
17
  export { configurePdfJs, getPdfJs } from "./utils/pdf";
18
18
  export { cn } from "./utils/cn";
19
- export type { TipTapEditorProps, UploadHandler, ToolbarMode, FixedToolbarProps, BlockHandleProps, SlashCommandMenuProps, PdfViewerProps, SlashMenuItem, } from "./types";
19
+ export type { TipTapEditorProps, UploadHandler, ToolbarMode, ToolbarFeature, FixedToolbarProps, BlockHandleProps, SlashCommandMenuProps, PdfViewerProps, SlashMenuItem, } from "./types";
20
+ export { TOOLBAR_PRESETS, resolveFeatures } from "./types";
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAGhC,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,aAAa,GACd,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAGhC,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -18,3 +18,4 @@ export { Column } from "./extensions/Column";
18
18
  export { sanitizeHtml, stripHtmlToExcerpt, transformLegacyHtml } from "./utils/sanitize";
19
19
  export { configurePdfJs, getPdfJs } from "./utils/pdf";
20
20
  export { cn } from "./utils/cn";
21
+ export { TOOLBAR_PRESETS, resolveFeatures } from "./types";