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"],
}),
);