Skip to content

Commit

Permalink
Merge pull request #335 from steven-tey/custom-upload
Browse files Browse the repository at this point in the history
feat: add custom upload config
  • Loading branch information
andrewdoro authored Mar 7, 2024
2 parents 2338cdc + 9534c6e commit 7499182
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 260 deletions.
137 changes: 137 additions & 0 deletions apps/docs/guides/image-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
title: "Image Upload (New)"
description: "Uploading images in the editor"
---

<Steps>

<Step title="Add image extension">
Configure image extension with your styling. The `imageClass` is used for styling the placeholder image.

```tsx
//extensions.ts
import { UploadImagesPlugin } from "novel/plugins";

const tiptapImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [
UploadImagesPlugin({
imageClass: cx("opacity-40 rounded-lg border border-stone-200"),
}),
];
},
}).configure({
allowBase64: true,
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
});

export const defaultExtensions = [
tiptapImage,
//other extensions
];

//editor.tsx
const Editor = () => {
return <EditorContent extensions={defaultExtensions} />
}

```

</Step>

<Step title="Create upload function">
`onUpload` should return a `Promise<string>`
`validateFn` is triggered before an image is uploaded. It should return a `boolean` value.

```tsx image-upload.ts
import { createImageUpload } from "novel/plugins";
import { toast } from "sonner";

const onUpload = async (file: File) => {
const promise = fetch("/api/upload", {
method: "POST",
headers: {
"content-type": file?.type || "application/octet-stream",
"x-vercel-filename": file?.name || "image.png",
},
body: file,
});

//This should return a src of the uploaded image
return promise;
};

export const uploadFn = createImageUpload({
onUpload,
validateFn: (file) => {
if (!file.type.includes("image/")) {
toast.error("File type not supported.");
return false;
} else if (file.size / 1024 / 1024 > 20) {
toast.error("File size too big (max 20MB).");
return false;
}
return true;
},
});

```
</Step>

<Step title="Configure events callbacks">
This is required to handle image paste and drop events in the editor.
```tsx editor.tsx
import { handleImageDrop, handleImagePaste } from "novel/plugins";
import { uploadFn } from "./image-upload";

...
<EditorContent
editorProps={{
handlePaste: (view, event) => handleImagePaste(view, event, uploadFn),
handleDrop: (view, event, _slice, moved) => handleImageDrop(view, event, moved, uploadFn),
...
}}
/>
...
```
</Step>

<Step title="Update slash-command suggestionsItems">


```tsx
import { ImageIcon } from "lucide-react";
import { createSuggestionItems } from "novel/extensions";
import { uploadFn } from "./image-upload";

export const suggestionItems = createSuggestionItems([
...,
{
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
// upload image
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
uploadFn(file, editor.view, pos);
}
};
input.click();
},
}
])
```

</Step>

</Steps>
20 changes: 1 addition & 19 deletions apps/docs/guides/tailwind/extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
StarterKit,
Placeholder,
} from "novel/extensions";
import { UploadImagesPlugin } from "novel/plugins";

import { cx } from "class-variance-authority";
import { slashCommand } from "./slash-command";
Expand All @@ -30,28 +29,11 @@ const placeholder = Placeholder;
const tiptapLink = TiptapLink.configure({
HTMLAttributes: {
class: cx(
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer"
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
),
},
});

const tiptapImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin()];
},
}).configure({
allowBase64: true,
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
});

const updatedImage = UpdatedImage.configure({
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
});

const taskList = TaskList.configure({
HTMLAttributes: {
class: cx("not-prose pl-2"),
Expand Down
94 changes: 34 additions & 60 deletions apps/docs/guides/tailwind/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ description: "Follow this guide to set up Novel with Tailwindcss"
---

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

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

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

<CardGroup cols={2}>
<Card title='Slash Command' href='/guides/tailwind/slash-command' icon='terminal'>
<Card
title="Slash Command"
href="/guides/tailwind/slash-command"
icon="terminal"
>
Slash commands are a way to quickly insert content into the editor.
</Card>
<Card title='Bubble Menu' href='/guides/tailwind/bubble-menu' icon='square-caret-down'>
<Card
title="Bubble Menu"
href="/guides/tailwind/bubble-menu"
icon="square-caret-down"
>
The bubble menu is a context menu that appears when you select text.
</Card>
</CardGroup>

## Add Editor Props

`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.

```tsx novel/src/editor.tsx
export const defaultEditorProps: EditorProviderProps["editorProps"] = {
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
}
},
},
handlePaste: (view, event) => {
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;

startImageUpload(file, view, pos);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
startImageUpload(file, view, coordinates?.pos || 0 - 1);
return true;
}
return false;
},
};
```
`handleCommandNavigation` is required for fixing the arrow navigation in the / command;

```tsx
import { handleCommandNavigation } from "novel/extensions";
import { defaultEditorProps, EditorContent } from "novel";

<EditorContent
...
editorProps={{
...defaultEditorProps,
attributes: {
class: `prose-lg prose-stone dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
},
}}
editorProps={{
handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event),
},
attributes: {
class: `prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
}
}}
/>
```

Expand Down Expand Up @@ -223,24 +197,24 @@ import { defaultEditorProps, EditorContent } from "novel";
ul[data-type="taskList"] li > label input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
background-color: var(--novel-white);
background-color: hsl(var(--background));
margin: 0;
cursor: pointer;
width: 1.2em;
height: 1.2em;
position: relative;
top: 5px;
border: 2px solid var(--novel-stone-900);
border: 2px solid hsl(var(--border));
margin-right: 0.3rem;
display: grid;
place-content: center;

&:hover {
background-color: var(--novel-stone-50);
background-color: hsl(var(--accent));
}

&:active {
background-color: var(--novel-stone-200);
background-color: hsl(var(--accent));
}

&::before {
Expand All @@ -260,7 +234,7 @@ import { defaultEditorProps, EditorContent } from "novel";
}

ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: var(--novel-stone-400);
color: var(--muted-foreground);
text-decoration: line-through;
text-decoration-thickness: 2px;
}
Expand Down
Loading

0 comments on commit 7499182

Please sign in to comment.