diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index f7d468347e..a1c858c358 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -11,9 +11,9 @@ import { updateBlock, } from "./api/blockManipulation/blockManipulation"; import { + HTMLToBlocks, blocksToHTML, blocksToMarkdown, - HTMLToBlocks, markdownToBlocks, } from "./api/formatConversions/formatConversions"; import { @@ -44,15 +44,13 @@ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFro import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; import { HyperlinkToolbarProsemirrorPlugin } from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; +import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin"; import { SideMenuProsemirrorPlugin } from "./extensions/SideMenu/SideMenuPlugin"; import { BaseSlashMenuItem } from "./extensions/SlashMenu/BaseSlashMenuItem"; import { SlashMenuProsemirrorPlugin } from "./extensions/SlashMenu/SlashMenuPlugin"; import { getDefaultSlashMenuItems } from "./extensions/SlashMenu/defaultSlashMenuItems"; import { UniqueID } from "./extensions/UniqueID/UniqueID"; import { mergeCSSClasses } from "./shared/utils"; -import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin"; - -const IMAGE_UPLOAD_API_KEY = ""; export type BlockNoteEditorOptions = { // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them. @@ -110,11 +108,11 @@ export type BlockNoteEditorOptions = { blockSchema: BSchema; /** - * A custom function to handle image uploads for the default image block. - * @param file The image file that should be uploaded. - * @returns The URL of the uploaded image. + * A custom function to handle file uploads. + * @param file The file that should be uploaded. + * @returns The URL of the uploaded file. */ - uploadImage: (file: File) => Promise; + uploadFile: (file: File) => Promise; /** * When enabled, allows for collaboration between multiple users. @@ -163,7 +161,7 @@ export class BlockNoteEditor { public readonly hyperlinkToolbar: HyperlinkToolbarProsemirrorPlugin; public readonly imageToolbar: ImageToolbarProsemirrorPlugin; - public readonly imageUpload: (file: File) => Promise; + public readonly uploadFile: ((file: File) => Promise) | undefined; constructor( private readonly options: Partial> = {} @@ -217,25 +215,7 @@ export class BlockNoteEditor { this.schema = newOptions.blockSchema; - this.imageUpload = async (file) => { - if (newOptions.uploadImage) { - return newOptions.uploadImage(file); - } - - // TODO: Proper backend - right now using imgbb.com since you can get an - // API key for free by just making an account. - // https://imgbb.com/ - const body = new FormData(); - body.append("key", IMAGE_UPLOAD_API_KEY); - body.append("image", file); - - return fetch("https://api.imgbb.com/1/upload?expiration=600", { - method: "POST", - body: body, - }) - .then((response) => response.json()) - .then((data) => data.data.url); - }; + this.uploadFile = newOptions.uploadFile; const initialContent = newOptions.initialContent || diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index f5cf430b8a..37bdadfdb9 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -49,8 +49,8 @@ describe("Simple ProseMirror Node Conversions", () => { expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode( - firstBlockConversion as any, + const firstNodeConversion = blockToNode( + firstBlockConversion, tt.schema ); @@ -150,8 +150,8 @@ describe("Complex ProseMirror Node Conversions", () => { expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode( - firstBlockConversion as any, + const firstNodeConversion = blockToNode( + firstBlockConversion, tt.schema ); diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 6be5ec9cf3..9241ac5aec 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -15,9 +15,9 @@ import { Styles, ToggledStyle, } from "../../extensions/Blocks/api/inlineContentTypes"; +import { getBlockInfo } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { UnreachableCaseError } from "../../shared/utils"; -import { getBlockInfo } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; const toggleStyles = new Set([ "bold", diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesOrg_DEV_ONLY.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesOrg_DEV_ONLY.ts new file mode 100644 index 0000000000..5944735c98 --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesOrg_DEV_ONLY.ts @@ -0,0 +1,13 @@ +export const uploadToTmpFilesOrg = async (file: File) => { + const body = new FormData(); + body.append("file", file); + + const ret = await fetch("https://tmpfiles.org/api/v1/upload", { + method: "POST", + body: body, + }); + return (await ret.json()).data.url.replace( + "tmpfiles.org/", + "tmpfiles.org/dl/" + ); +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 26bd8eff8e..68078fecfa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,6 +8,7 @@ export * from "./extensions/Blocks/api/inlineContentTypes"; export * from "./extensions/Blocks/api/selectionTypes"; export * from "./extensions/Blocks/api/serialization"; export * as blockStyles from "./extensions/Blocks/nodes/Block.module.css"; +export * from "./extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesOrg_DEV_ONLY"; export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; export * from "./extensions/ImageToolbar/ImageToolbarPlugin"; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ReplaceImageButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ReplaceImageButton.tsx index 7813e750a9..2169e3ff7e 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ReplaceImageButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ReplaceImageButton.tsx @@ -1,11 +1,11 @@ import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; -import { useEffect, useMemo, useState } from "react"; -import { RiImageEditFill } from "react-icons/ri"; import Tippy from "@tippyjs/react"; +import { useEffect, useState } from "react"; +import { RiImageEditFill } from "react-icons/ri"; +import { DefaultImageToolbar } from "../../../ImageToolbar/components/DefaultImageToolbar"; import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; -import { DefaultImageToolbar } from "../../../ImageToolbar/components/DefaultImageToolbar"; export const ReplaceImageButton = (props: { editor: BlockNoteEditor; @@ -18,14 +18,11 @@ export const ReplaceImageButton = (props: { setIsOpen(false); }, [selectedBlocks]); - const show = useMemo( - () => - // Checks if only one block is selected. - selectedBlocks.length === 1 && - // Checks if the selected block is an image. - selectedBlocks[0].type === "image", - [selectedBlocks] - ); + const show = + // Checks if only one block is selected. + selectedBlocks.length === 1 && + // Checks if the selected block is an image. + selectedBlocks[0].type === "image"; if (!show) { return null; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx index 6e9cb6a98e..cf89ecef97 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx @@ -1,10 +1,10 @@ -import { useCallback, useMemo } from "react"; import { BlockNoteEditor, BlockSchema, DefaultProps, PartialBlock, } from "@blocknote/core"; +import { useCallback, useMemo } from "react"; import { IconType } from "react-icons"; import { RiAlignCenter, @@ -55,13 +55,7 @@ export const TextAlignButton = (props: { ); const show = useMemo(() => { - for (const block of selectedBlocks) { - if ("textAlignment" in block.props) { - return true; - } - } - - return false; + return !!selectedBlocks.find((block) => "textAlignment" in block.props); }, [selectedBlocks]); if (!show) { diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx index e8a9647436..c71824e873 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from "react"; import { BlockNoteEditor, BlockSchema, ToggledStyle } from "@blocknote/core"; +import { useMemo, useState } from "react"; import { IconType } from "react-icons"; import { RiBold, @@ -10,8 +10,8 @@ import { } from "react-icons/ri"; import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; -import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; import { useEditorChange } from "../../../hooks/useEditorChange"; +import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; import { formatKeyboardShortcut } from "../../../utils"; const shortcuts: Record = { @@ -50,13 +50,7 @@ export const ToggledStyleButton = (props: { }; const show = useMemo(() => { - for (const block of selectedBlocks) { - if (block.content !== undefined) { - return true; - } - } - - return false; + return !!selectedBlocks.find((block) => block.content !== undefined); }, [selectedBlocks]); if (!show) { diff --git a/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx b/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx index 94fdffc04b..4f12da2bc2 100644 --- a/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx +++ b/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx @@ -14,21 +14,23 @@ import { export const DefaultImageToolbar = ( props: ImageToolbarProps ) => { - const [openTab, setOpenTab] = useState<"upload" | "embed">("upload"); + const [openTab, setOpenTab] = useState<"upload" | "embed">(props.editor.uploadFile !== undefined ? "upload" : "embed"); const [uploading, setUploading] = useState(false); + const handleFileChange = useCallback( - (file: File) => { + async (file: File) => { setUploading(true); - props.editor.imageUpload(file).then((url) => { + if (props.editor.uploadFile !== undefined) { + const uploaded = await props.editor.uploadFile(file); props.editor.updateBlock(props.block, { type: "image", props: { - url: url, + url: uploaded, }, } as PartialBlock); setUploading(false); - }); + } }, [props.block, props.editor] ); @@ -75,18 +77,22 @@ export const DefaultImageToolbar = ( {uploading && } - Upload + {props.editor.uploadFile !== undefined && ( + Upload + )} Embed - - - + {props.editor.uploadFile !== undefined && ( + + + + )}