Skip to content

Commit 7499182

Browse files
authored
Merge pull request #335 from steven-tey/custom-upload
feat: add custom upload config
2 parents 2338cdc + 9534c6e commit 7499182

File tree

14 files changed

+387
-260
lines changed

14 files changed

+387
-260
lines changed

apps/docs/guides/image-upload.mdx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
title: "Image Upload (New)"
3+
description: "Uploading images in the editor"
4+
---
5+
6+
<Steps>
7+
8+
<Step title="Add image extension">
9+
Configure image extension with your styling. The `imageClass` is used for styling the placeholder image.
10+
11+
```tsx
12+
//extensions.ts
13+
import { UploadImagesPlugin } from "novel/plugins";
14+
15+
const tiptapImage = TiptapImage.extend({
16+
addProseMirrorPlugins() {
17+
return [
18+
UploadImagesPlugin({
19+
imageClass: cx("opacity-40 rounded-lg border border-stone-200"),
20+
}),
21+
];
22+
},
23+
}).configure({
24+
allowBase64: true,
25+
HTMLAttributes: {
26+
class: cx("rounded-lg border border-muted"),
27+
},
28+
});
29+
30+
export const defaultExtensions = [
31+
tiptapImage,
32+
//other extensions
33+
];
34+
35+
//editor.tsx
36+
const Editor = () => {
37+
return <EditorContent extensions={defaultExtensions} />
38+
}
39+
40+
```
41+
42+
</Step>
43+
44+
<Step title="Create upload function">
45+
`onUpload` should return a `Promise<string>`
46+
`validateFn` is triggered before an image is uploaded. It should return a `boolean` value.
47+
48+
```tsx image-upload.ts
49+
import { createImageUpload } from "novel/plugins";
50+
import { toast } from "sonner";
51+
52+
const onUpload = async (file: File) => {
53+
const promise = fetch("/api/upload", {
54+
method: "POST",
55+
headers: {
56+
"content-type": file?.type || "application/octet-stream",
57+
"x-vercel-filename": file?.name || "image.png",
58+
},
59+
body: file,
60+
});
61+
62+
//This should return a src of the uploaded image
63+
return promise;
64+
};
65+
66+
export const uploadFn = createImageUpload({
67+
onUpload,
68+
validateFn: (file) => {
69+
if (!file.type.includes("image/")) {
70+
toast.error("File type not supported.");
71+
return false;
72+
} else if (file.size / 1024 / 1024 > 20) {
73+
toast.error("File size too big (max 20MB).");
74+
return false;
75+
}
76+
return true;
77+
},
78+
});
79+
80+
```
81+
</Step>
82+
83+
<Step title="Configure events callbacks">
84+
This is required to handle image paste and drop events in the editor.
85+
```tsx editor.tsx
86+
import { handleImageDrop, handleImagePaste } from "novel/plugins";
87+
import { uploadFn } from "./image-upload";
88+
89+
...
90+
<EditorContent
91+
editorProps={{
92+
handlePaste: (view, event) => handleImagePaste(view, event, uploadFn),
93+
handleDrop: (view, event, _slice, moved) => handleImageDrop(view, event, moved, uploadFn),
94+
...
95+
}}
96+
/>
97+
...
98+
```
99+
</Step>
100+
101+
<Step title="Update slash-command suggestionsItems">
102+
103+
104+
```tsx
105+
import { ImageIcon } from "lucide-react";
106+
import { createSuggestionItems } from "novel/extensions";
107+
import { uploadFn } from "./image-upload";
108+
109+
export const suggestionItems = createSuggestionItems([
110+
...,
111+
{
112+
title: "Image",
113+
description: "Upload an image from your computer.",
114+
searchTerms: ["photo", "picture", "media"],
115+
icon: <ImageIcon size={18} />,
116+
command: ({ editor, range }) => {
117+
editor.chain().focus().deleteRange(range).run();
118+
// upload image
119+
const input = document.createElement("input");
120+
input.type = "file";
121+
input.accept = "image/*";
122+
input.onchange = async () => {
123+
if (input.files?.length) {
124+
const file = input.files[0];
125+
const pos = editor.view.state.selection.from;
126+
uploadFn(file, editor.view, pos);
127+
}
128+
};
129+
input.click();
130+
},
131+
}
132+
])
133+
```
134+
135+
</Step>
136+
137+
</Steps>

apps/docs/guides/tailwind/extensions.mdx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
StarterKit,
1919
Placeholder,
2020
} from "novel/extensions";
21-
import { UploadImagesPlugin } from "novel/plugins";
2221

2322
import { cx } from "class-variance-authority";
2423
import { slashCommand } from "./slash-command";
@@ -30,28 +29,11 @@ const placeholder = Placeholder;
3029
const tiptapLink = TiptapLink.configure({
3130
HTMLAttributes: {
3231
class: cx(
33-
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer"
32+
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
3433
),
3534
},
3635
});
3736

38-
const tiptapImage = TiptapImage.extend({
39-
addProseMirrorPlugins() {
40-
return [UploadImagesPlugin()];
41-
},
42-
}).configure({
43-
allowBase64: true,
44-
HTMLAttributes: {
45-
class: cx("rounded-lg border border-muted"),
46-
},
47-
});
48-
49-
const updatedImage = UpdatedImage.configure({
50-
HTMLAttributes: {
51-
class: cx("rounded-lg border border-muted"),
52-
},
53-
});
54-
5537
const taskList = TaskList.configure({
5638
HTMLAttributes: {
5739
class: cx("not-prose pl-2"),

apps/docs/guides/tailwind/setup.mdx

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ description: "Follow this guide to set up Novel with Tailwindcss"
44
---
55

66
<Info>
7-
This example demonstrates the use of Shadcn-ui for ui, but alternative libraries and components
8-
can also be employed.
7+
This example demonstrates the use of Shadcn-ui for ui, but alternative
8+
libraries and components can also be employed.
99
</Info>
1010

11-
<Card title='Shadcn-ui' icon='link' href='https://ui.shadcn.com/docs/installation'>
12-
You can find more info about installing shadcn-ui here. You will need to add the following
13-
components: <b>Button, Separator, Popover, Command, Dialog,</b>
11+
<Card
12+
title="Shadcn-ui"
13+
icon="link"
14+
href="https://ui.shadcn.com/docs/installation"
15+
>
16+
You can find more info about installing shadcn-ui here. You will need to add
17+
the following components: <b>Button, Separator, Popover, Command, Dialog,</b>
1418
</Card>
1519

1620
This example will use the same stucture from here: [Anatomy](/quickstart#anatomy)\
@@ -76,70 +80,40 @@ You can find the full example here: [Tailwind Example](https://github.com/steven
7680
## Create Menus
7781

7882
<CardGroup cols={2}>
79-
<Card title='Slash Command' href='/guides/tailwind/slash-command' icon='terminal'>
83+
<Card
84+
title="Slash Command"
85+
href="/guides/tailwind/slash-command"
86+
icon="terminal"
87+
>
8088
Slash commands are a way to quickly insert content into the editor.
8189
</Card>
82-
<Card title='Bubble Menu' href='/guides/tailwind/bubble-menu' icon='square-caret-down'>
90+
<Card
91+
title="Bubble Menu"
92+
href="/guides/tailwind/bubble-menu"
93+
icon="square-caret-down"
94+
>
8395
The bubble menu is a context menu that appears when you select text.
8496
</Card>
8597
</CardGroup>
8698

8799
## Add Editor Props
88100

89-
`defaultEditorProps` are required to fix the slash command keyboard navigation. For any custom use case you can write your own or extend the default props.
90-
91-
```tsx novel/src/editor.tsx
92-
export const defaultEditorProps: EditorProviderProps["editorProps"] = {
93-
handleDOMEvents: {
94-
keydown: (_view, event) => {
95-
// prevent default event listeners from firing when slash command is active
96-
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
97-
const slashCommand = document.querySelector("#slash-command");
98-
if (slashCommand) {
99-
return true;
100-
}
101-
}
102-
},
103-
},
104-
handlePaste: (view, event) => {
105-
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
106-
event.preventDefault();
107-
const file = event.clipboardData.files[0];
108-
const pos = view.state.selection.from;
109-
110-
startImageUpload(file, view, pos);
111-
return true;
112-
}
113-
return false;
114-
},
115-
handleDrop: (view, event, _slice, moved) => {
116-
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
117-
event.preventDefault();
118-
const file = event.dataTransfer.files[0];
119-
const coordinates = view.posAtCoords({
120-
left: event.clientX,
121-
top: event.clientY,
122-
});
123-
// here we deduct 1 from the pos or else the image will create an extra node
124-
startImageUpload(file, view, coordinates?.pos || 0 - 1);
125-
return true;
126-
}
127-
return false;
128-
},
129-
};
130-
```
101+
`handleCommandNavigation` is required for fixing the arrow navigation in the / command;
131102

132103
```tsx
104+
import { handleCommandNavigation } from "novel/extensions";
133105
import { defaultEditorProps, EditorContent } from "novel";
134106

135107
<EditorContent
136108
...
137-
editorProps={{
138-
...defaultEditorProps,
139-
attributes: {
140-
class: `prose-lg prose-stone dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
141-
},
142-
}}
109+
editorProps={{
110+
handleDOMEvents: {
111+
keydown: (_view, event) => handleCommandNavigation(event),
112+
},
113+
attributes: {
114+
class: `prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
115+
}
116+
}}
143117
/>
144118
```
145119

@@ -223,24 +197,24 @@ import { defaultEditorProps, EditorContent } from "novel";
223197
ul[data-type="taskList"] li > label input[type="checkbox"] {
224198
-webkit-appearance: none;
225199
appearance: none;
226-
background-color: var(--novel-white);
200+
background-color: hsl(var(--background));
227201
margin: 0;
228202
cursor: pointer;
229203
width: 1.2em;
230204
height: 1.2em;
231205
position: relative;
232206
top: 5px;
233-
border: 2px solid var(--novel-stone-900);
207+
border: 2px solid hsl(var(--border));
234208
margin-right: 0.3rem;
235209
display: grid;
236210
place-content: center;
237211

238212
&:hover {
239-
background-color: var(--novel-stone-50);
213+
background-color: hsl(var(--accent));
240214
}
241215

242216
&:active {
243-
background-color: var(--novel-stone-200);
217+
background-color: hsl(var(--accent));
244218
}
245219

246220
&::before {
@@ -260,7 +234,7 @@ import { defaultEditorProps, EditorContent } from "novel";
260234
}
261235

262236
ul[data-type="taskList"] li[data-checked="true"] > div > p {
263-
color: var(--novel-stone-400);
237+
color: var(--muted-foreground);
264238
text-decoration: line-through;
265239
text-decoration-thickness: 2px;
266240
}

0 commit comments

Comments
 (0)