Skip to content

Commit

Permalink
feat: add custom upload config
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewdoro committed Mar 6, 2024
1 parent 4d49c2f commit a202e6e
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 153 deletions.
13 changes: 10 additions & 3 deletions apps/web/components/tailwind/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { defaultEditorContent } from "@/lib/content";
import React, { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import {
defaultEditorProps,
EditorInstance,
EditorRoot,
EditorBubble,
Expand All @@ -13,7 +12,7 @@ import {
EditorContent,
type JSONContent,
} from "novel";
import { ImageResizer } from "novel/extensions";
import { ImageResizer, handleCommandNavigation } from "novel/extensions";
import { defaultExtensions } from "./extensions";
import { Separator } from "./ui/separator";
import { NodeSelector } from "./selectors/node-selector";
Expand All @@ -22,6 +21,8 @@ import { ColorSelector } from "./selectors/color-selector";

import { TextButtons } from "./selectors/text-buttons";
import { slashCommand, suggestionItems } from "./slash-command";
import { handleImageDrop, handleImagePaste } from "novel/plugins";
import { uploadFn } from "./image-upload";

const extensions = [...defaultExtensions, slashCommand];

Expand Down Expand Up @@ -64,7 +65,13 @@ const TailwindEditor = () => {
extensions={extensions}
className="relative min-h-[500px] w-full max-w-screen-lg border-muted bg-background sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg"
editorProps={{
...defaultEditorProps,
handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event),
},
handlePaste: (view, event) =>
handleImagePaste(view, event, uploadFn),
handleDrop: (view, event, _slice, moved) =>
handleImageDrop(view, event, moved, uploadFn),
attributes: {
class: `prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full`,
},
Expand Down
6 changes: 5 additions & 1 deletion apps/web/components/tailwind/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ const tiptapLink = TiptapLink.configure({

const tiptapImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin()];
return [
UploadImagesPlugin({
imageClass: cx("opacity-40 rounded-lg border border-stone-200"),
}),
];
},
}).configure({
allowBase64: true,
Expand Down
56 changes: 56 additions & 0 deletions apps/web/components/tailwind/image-upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createImageUpload } from "novel/plugins";
import { toast } from "sonner";

const onUpload = (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,
});
return new Promise((resolve) => {
toast.promise(
promise.then(async (res) => {
// Successfully uploaded image
if (res.status === 200) {
const { url } = (await res.json()) as any;
// preload the image
let image = new Image();
image.src = url;
image.onload = () => {
resolve(url);
};
// No blob store configured
} else if (res.status === 401) {
resolve(file);
throw new Error(
"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead.",
);
// Unknown error
} else {
throw new Error(`Error uploading image. Please try again.`);
}
}),
{
loading: "Uploading image...",
success: "Image uploaded successfully.",
error: (e) => e.message,
},
);
});
};

export const uploadFn = createImageUpload({
onUpload,
validateFn: (file) => {
if (!file.type.includes("image/")) {
toast.error("File type not supported.");
return;
} else if (file.size / 1024 / 1024 > 20) {
toast.error("File size too big (max 20MB).");
return;
}
},
});
4 changes: 2 additions & 2 deletions apps/web/components/tailwind/slash-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
TextQuote,
} from "lucide-react";
import { createSuggestionItems } from "novel/extensions";
import { startImageUpload } from "novel/plugins";
import { Command, renderItems } from "novel/extensions";
import { uploadFn } from "./image-upload";

export const suggestionItems = createSuggestionItems([
{
Expand Down Expand Up @@ -145,7 +145,7 @@ export const suggestionItems = createSuggestionItems([
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos);
uploadFn(file, editor.view, pos);
}
};
input.click();
Expand Down
41 changes: 1 addition & 40 deletions packages/headless/src/components/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { EditorProvider } from "@tiptap/react";
import { Provider } from "jotai";
import tunnel from "tunnel-rat";
import { simpleExtensions } from "../extensions";
import { startImageUpload } from "../plugins/upload-images";
import { novelStore } from "../utils/store";
import { EditorCommandTunnelContext } from "./editor-command";
import type { FC, ReactNode } from "react";
import type { EditorProviderProps, JSONContent } from "@tiptap/react";
import type { EditorView } from "@tiptap/pm/view";

export interface EditorProps {
readonly children: ReactNode;
Expand Down Expand Up @@ -57,42 +57,3 @@ export const EditorContent = forwardRef<HTMLDivElement, EditorContentProps>(
);

EditorContent.displayName = "EditorContent";

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?.files.length) {
event.preventDefault();
const [file] = Array.from(event.clipboardData.files);
const pos = view.state.selection.from;

if (file) startImageUpload(file, view, pos);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (!moved && event.dataTransfer?.files.length) {
event.preventDefault();
const [file] = Array.from(event.dataTransfer.files);
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
if (file) startImageUpload(file, view, coordinates?.pos ?? 0 - 1);
return true;
}
return false;
},
};
7 changes: 1 addition & 6 deletions packages/headless/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ export { useCurrentEditor as useEditor } from "@tiptap/react";
export { type Editor as EditorInstance } from "@tiptap/core";
export type { JSONContent } from "@tiptap/react";

export {
EditorRoot,
EditorContent,
type EditorContentProps,
defaultEditorProps,
} from "./editor";
export { EditorRoot, EditorContent, type EditorContentProps } from "./editor";
export { EditorBubble } from "./editor-bubble";
export { EditorBubbleItem } from "./editor-bubble-item";
export { EditorCommand } from "./editor-command";
Expand Down
9 changes: 9 additions & 0 deletions packages/headless/src/extensions/slash-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,13 @@ export interface SuggestionItem {

export const createSuggestionItems = (items: SuggestionItem[]) => items;

export const handleCommandNavigation = (event: KeyboardEvent) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
}
};

export { Command, renderItems };
7 changes: 5 additions & 2 deletions packages/headless/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export {
UploadImagesPlugin,
startImageUpload,
handleImageUpload,
type UploadFn,
type ImageUploadOptions,
createImageUpload,
handleImageDrop,
handleImagePaste,
} from "./upload-images";
Loading

0 comments on commit a202e6e

Please sign in to comment.