import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfig, createBlockConfig, createBlockSpec, } from "../../schema/index.js"; import { defaultProps, parseDefaultProps } from "../defaultProps.js"; import { parseFigureElement } from "../File/helpers/parse/parseFigureElement.js"; import { createResizableFileBlockWrapper } from "../File/helpers/render/createResizableFileBlockWrapper.js"; import { createFigureWithCaption } from "../File/helpers/toExternalHTML/createFigureWithCaption.js"; import { createLinkWithCaption } from "../File/helpers/toExternalHTML/createLinkWithCaption.js"; import { parseImageElement } from "./parseImageElement.js"; export const FILE_IMAGE_ICON_SVG = ''; export interface ImageOptions { icon?: string; } export type ImageBlockConfig = ReturnType; export const createImageBlockConfig = createBlockConfig( (_ctx: ImageOptions = {}) => ({ type: "image" as const, propSchema: { textAlignment: defaultProps.textAlignment, backgroundColor: defaultProps.backgroundColor, // File name. name: { default: "" as const, }, // File url. url: { default: "" as const, }, // File caption. caption: { default: "" as const, }, showPreview: { default: true, }, // File preview width in px. previewWidth: { default: undefined, type: "number" as const, }, }, content: "none" as const, }) as const, ); export const imageParse = (_config: ImageOptions = {}) => (element: HTMLElement) => { if (element.tagName === "IMG") { // Ignore if parent figure has already been parsed. if (element.closest("figure")) { return undefined; } const { backgroundColor } = parseDefaultProps(element); return { ...parseImageElement(element as HTMLImageElement), backgroundColor, }; } if (element.tagName === "FIGURE") { const parsedFigure = parseFigureElement(element, "img"); if (!parsedFigure) { return undefined; } const { targetElement, caption } = parsedFigure; const { backgroundColor } = parseDefaultProps(element); return { ...parseImageElement(targetElement as HTMLImageElement), backgroundColor, caption, }; } return undefined; }; export const imageRender = (config: ImageOptions = {}) => ( block: BlockFromConfig, any, any>, editor: BlockNoteEditor< Record<"image", ReturnType>, any, any >, ) => { const icon = document.createElement("div"); icon.innerHTML = config.icon ?? FILE_IMAGE_ICON_SVG; const imageWrapper = document.createElement("div"); imageWrapper.className = "bn-visual-media-wrapper"; const image = document.createElement("img"); image.className = "bn-visual-media"; if (editor.resolveFileUrl) { editor.resolveFileUrl(block.props.url).then((downloadUrl) => { image.src = downloadUrl; }); } else { image.src = block.props.url; } // alt describes image content (per WCAG H86); figcaption (when present) // is the contextual caption. Fall back to "" so unlabelled images are // marked decorative rather than getting a noisy generic fallback. image.alt = block.props.name || ""; image.contentEditable = "false"; image.draggable = false; if (block.props.previewWidth) { image.width = block.props.previewWidth; } imageWrapper.appendChild(image); return createResizableFileBlockWrapper( block, editor, { dom: imageWrapper }, imageWrapper, icon.firstElementChild as HTMLElement, ); }; export const imageToExternalHTML = (_config: ImageOptions = {}) => ( block: BlockFromConfig, any, any>, _editor: BlockNoteEditor< Record<"image", ReturnType>, any, any >, ) => { if (!block.props.url) { return { dom: document.createElement("img"), }; } let image; if (block.props.showPreview) { image = document.createElement("img"); image.src = block.props.url; image.alt = block.props.name || ""; if (block.props.previewWidth) { image.width = block.props.previewWidth; } } else { image = document.createElement("a"); image.href = block.props.url; image.textContent = block.props.name || block.props.url; } if (block.props.caption) { if (block.props.showPreview) { return createFigureWithCaption(image, block.props.caption); } else { return createLinkWithCaption(image, block.props.caption); } } return { dom: image, }; }; export const createImageBlockSpec = createBlockSpec( createImageBlockConfig, (config) => ({ meta: { fileBlockAccept: ["image/*"], }, parse: imageParse(config), render: imageRender(config), toExternalHTML: imageToExternalHTML(config), runsBefore: ["file"], }), );