TypeCellOS / BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
https://www.blocknotejs.org/
Mozilla Public License 2.0
6.76k stars 471 forks source link

Custom block with FileUpload and content #1264

Open serpent213 opened 4 days ago

serpent213 commented 4 days ago

As part of a website-CMS I needed a way to define a background image for an element and overlay two blocks of text. This is what I came up with:

Screenshot 2024-11-20 at 15 07 43 Screenshot 2024-11-20 at 15 07 33
// import type { FileBlockConfig } from "@blocknote/core"
import { createReactBlockSpec, FilePanel } from "@blocknote/react"
import { useEffect, useRef, useState } from "react"

// interface FileAndContentBlockConfig extends Omit<FileBlockConfig, "content"> {
//     content: "inline"
// }

export const BackgroundScroller = createReactBlockSpec(
    {
        type: "backgroundScroller",
        propSchema: {
            // caption: {
            //     default: ""
            // },
            heading: {
                default: ""
            },
            url: {
                default: ""
            },
            name: {
                default: ""
            }
        },
        content: "inline",
        // isFileBlock: true,
        fileBlockAccept: ["image/*"]
    },
    {
        render: (props) => {
            const [isFilePanelOpen, setIsFilePanelOpen] = useState(false)
            const inputClassName = "block !p-2 !text-center !text-white bg-gray-600/60 rounded-lg"
            const filePanelRef = useRef<HTMLDivElement>(null)

            // Update non-content props
            const handleUpdate = (propName: "heading") => (e: React.ChangeEvent<HTMLInputElement>) => {
                props.editor.updateBlock(props.block, {
                    type: "backgroundScroller",
                    props: {
                        ...props.block.props,
                        [propName]: e.target.value
                    }
                })
            }

            // Close file panel when clicking outside of it
            useEffect(() => {
                const handleClickOutside = (event: MouseEvent | TouchEvent) => {
                    if (filePanelRef.current && !filePanelRef.current.contains(event.target as Node)) {
                        setIsFilePanelOpen(false)
                    }
                }

                if (isFilePanelOpen) {
                    document.addEventListener("mousedown", handleClickOutside)
                    document.addEventListener("touchstart", handleClickOutside)
                } else {
                    document.removeEventListener("mousedown", handleClickOutside)
                    document.removeEventListener("touchstart", handleClickOutside)
                }

                return () => {
                    document.removeEventListener("mousedown", handleClickOutside)
                    document.removeEventListener("touchstart", handleClickOutside)
                }
            }, [isFilePanelOpen])

            // Close file panel after file upload
            // biome-ignore lint/correctness/useExhaustiveDependencies: 🤷🏼‍♂️
            useEffect(() => {
                setIsFilePanelOpen(false)
            }, [props.block.props.url])

            return (
                <div
                    className="relative min-h-52 !p-0 flex flex-col justify-center items-center flex-grow rounded space-y-3 bg-lime-500"
                    style={
                        props.block.props.url && (props.block.props.url as string).length > 0
                            ? {
                                  backgroundImage: `url('${props.block.props.url}')`,
                                  backgroundSize: "cover",
                                  backgroundPosition: "center",
                                  backgroundRepeat: "no-repeat"
                              }
                            : {}
                    }
                >
                    <input
                        type="text"
                        value={props.block.props.heading}
                        placeholder="Heading"
                        className={inputClassName}
                        size={(props.block.props.heading && (props.block.props.heading as string).length) || 10}
                        onChange={handleUpdate("heading")}
                    />
                    <div
                        className={
                            "inline-content text-2xl font-medium text-center text-white bg-gray-600/60 p-2 rounded-lg"
                        }
                        ref={props.contentRef}
                    />
                    <button
                        type="button"
                        className="absolute top-2 right-2 !m-0 !btn !btn-sm !btn-primary"
                        onClick={() => setIsFilePanelOpen(true)}
                    >
                        Bild hochladen
                    </button>
                    {isFilePanelOpen && (
                        <div className="absolute top-9 right-2" ref={filePanelRef}>
                            {/* @ts-ignore */}
                            <FilePanel block={props.block} />
                        </div>
                    )}
                </div>
            )
        }
    }
)

This seems to work fine, but feels slightly hacky: I had to suppress one TS and one Biome error and probably there is a better way to implement the handleClickOutside. Would be lovely to have an example in this direction. 🙂