Skip to content

Custom block with FileUpload and content #1264

@serpent213

Description

@serpent213

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions