From e7db2c53f071426c37c69924a2eed7ad528bd967 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 17 May 2024 12:16:51 +0200 Subject: [PATCH 1/7] wip --- examples/01-basic/testing/App.tsx | 7 +- .../src/api/testUtil/cases/customBlocks.ts | 4 +- .../FileBlockContent/FileBlockContent.ts | 92 +++++- .../FileBlockContent/ImageBlockContent.ts | 119 ++++++++ .../utils/renderWithResizeHandles.ts | 3 +- .../FileBlockContent/fileBlockConfig.ts | 37 --- .../FileBlockContent/fileBlockExtension.ts | 39 --- .../FileBlockContent/fileBlockHelpers.ts | 227 ++++++++++++++ .../fileBlockImplementation.ts | 278 ------------------ packages/core/src/blocks/defaultBlocks.ts | 7 +- packages/core/src/editor/Block.css | 36 ++- .../getDefaultSlashMenuItems.ts | 103 ++++--- packages/core/src/index.ts | 13 +- packages/core/src/schema/blocks/types.ts | 38 ++- .../extensions/utils/ResizeHandlesWrapper.tsx | 1 - .../FilePanel/DefaultTabs/EmbedTab.tsx | 2 - .../FilePanel/DefaultTabs/UploadTab.tsx | 5 +- 17 files changed, 555 insertions(+), 456 deletions(-) create mode 100644 packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/fileBlockConfig.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/fileBlockExtension.ts create mode 100644 packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/fileBlockImplementation.ts diff --git a/examples/01-basic/testing/App.tsx b/examples/01-basic/testing/App.tsx index efe1e8b78d..4045053138 100644 --- a/examples/01-basic/testing/App.tsx +++ b/examples/01-basic/testing/App.tsx @@ -4,18 +4,13 @@ import { uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; -import { - createReactFileBlock, - defaultReactFileExtensions, - useCreateBlockNote, -} from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, - file: createReactFileBlock(defaultReactFileExtensions), }, }); diff --git a/packages/core/src/api/testUtil/cases/customBlocks.ts b/packages/core/src/api/testUtil/cases/customBlocks.ts index 4161f319fb..0466f9f09c 100644 --- a/packages/core/src/api/testUtil/cases/customBlocks.ts +++ b/packages/core/src/api/testUtil/cases/customBlocks.ts @@ -1,5 +1,7 @@ import { EditorTestCases } from "../index"; +import { filePropSchema } from "../../../blocks/FileBlockContent/fileBlockConfig"; +import { fileRender } from "../../../blocks/FileBlockContent/fileBlockHelpers"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; import { DefaultInlineContentSchema, @@ -10,8 +12,6 @@ import { defaultProps } from "../../../blocks/defaultProps"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; import { BlockNoteSchema } from "../../../editor/BlockNoteSchema"; import { createBlockSpec } from "../../../schema"; -import { fileRender } from "../../../blocks/FileBlockContent/fileBlockImplementation"; -import { filePropSchema } from "../../../blocks/FileBlockContent/fileBlockConfig"; // This is a modified version of the default file block that does not implement // a `toExternalHTML` function. It's used to test if the custom serializer by diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index fa389eb075..235383a378 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -1,12 +1,86 @@ -import { createBlockSpec } from "../../schema"; -import { fileBlockConfig } from "./fileBlockConfig"; -import { createFileBlockImplementation } from "./fileBlockImplementation"; -import { FileBlockExtension } from "./fileBlockExtension"; - -export const createFileBlock = ( - extensions?: Record -) => - createBlockSpec(fileBlockConfig, createFileBlockImplementation(extensions)); +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { + BlockFromConfig, + FileBlockConfig, + PropSchema, + createBlockSpec, +} from "../../schema"; +import { defaultProps } from "../defaultProps"; + +import { + createFileAndCaptionDOM, + createFileIconAndNameDOM, + createFilePlaceholderDOM, + fileParse, + fileToExternalHTML, +} from "./fileBlockHelpers"; + +export const filePropSchema = { + textAlignment: defaultProps.textAlignment, + backgroundColor: defaultProps.backgroundColor, + // File name. + name: { + default: "" as const, + }, + // File url. + url: { + default: "" as const, + }, + // File caption. + caption: { + default: "" as const, + }, +} satisfies PropSchema; + +export const fileBlockConfig = { + type: "file" as const, + propSchema: filePropSchema, + content: "none", + isFileBlock: true, +} satisfies FileBlockConfig; + +export const fileRender = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + // Wrapper element to set the file alignment, contains both file/file + // upload dashboard and caption. + const wrapper = document.createElement("div"); + wrapper.className = "bn-file-block-content-wrapper"; + + // const fileType = block.props.fileType; + // block.props.showPreview && fileType && extensions && fileType in extensions + // ? extensions[fileType].render(block, editor) + // : defaultFileRender(block); + + // File element. + + if (block.props.url === "") { + const placeholder = createFilePlaceholderDOM(block, editor); + wrapper.appendChild(placeholder.dom); + + return { + dom: wrapper, + destroy: () => { + placeholder?.destroy?.(); + }, + }; + } else { + const file = createFileIconAndNameDOM(block).dom; + const element = createFileAndCaptionDOM(block, editor, file); + wrapper.appendChild(element.dom); + + return { + dom: wrapper, + }; + } +}; + +export const FileBlock = createBlockSpec(fileBlockConfig, { + render: (block, editor) => fileRender(block, editor), + parse: (element) => fileParse(element), + toExternalHTML: (block, editor) => fileToExternalHTML(block, editor), +}); // - React support? // - Support parse HTML and toExternalHTML diff --git a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts new file mode 100644 index 0000000000..e7cccf68ed --- /dev/null +++ b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts @@ -0,0 +1,119 @@ +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { + BlockFromConfig, + FileBlockConfig, + PropSchema, + createBlockSpec, +} from "../../schema"; +import { defaultProps } from "../defaultProps"; +import { renderWithResizeHandles } from "./extensions/utils/renderWithResizeHandles"; + +import { + createFileAndCaptionDOM, + createFileIconAndNameDOM, + createFilePlaceholderDOM, + fileParse, + fileToExternalHTML, +} from "./fileBlockHelpers"; + +export 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: 512, + }, +} satisfies PropSchema; + +export const imageBlockConfig = { + type: "image" as const, + propSchema, + content: "none", + isFileBlock: true, +} satisfies FileBlockConfig; + +export const fileRender = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + // Wrapper element to set the file alignment, contains both file/file + // upload dashboard and caption. + const wrapper = document.createElement("div"); + wrapper.className = "bn-file-block-content-wrapper"; + + // const fileType = block.props.fileType; + // block.props.showPreview && fileType && extensions && fileType in extensions + // ? extensions[fileType].render(block, editor) + // : defaultFileRender(block); + + // File element. + + debugger; + if (block.props.url === "") { + const placeholder = createFilePlaceholderDOM(block, editor); + wrapper.appendChild(placeholder.dom); + + return { + dom: wrapper, + destroy: () => { + placeholder?.destroy?.(); + }, + }; + } else if (!block.props.showPreview) { + const file = createFileIconAndNameDOM(block).dom; + const element = createFileAndCaptionDOM(block, editor, file); + + return { + dom: element.dom, + }; + } else { + const image = document.createElement("img"); + image.className = "bn-visual-media"; + image.src = block.props.url; + image.alt = block.props.caption || "BlockNote image"; + image.contentEditable = "false"; + image.draggable = false; + image.width = Math.min( + block.props.previewWidth, + editor.domElement.firstElementChild!.clientWidth + ); + + const file = renderWithResizeHandles( + block, + editor, + image, + () => image.width, + (width) => (image.width = width) + ); + + const element = createFileAndCaptionDOM(block, editor, file.dom); + wrapper.appendChild(element.dom); + + return { + dom: wrapper, + destroy: file.destroy, + }; + } +}; + +export const ImageBlock = createBlockSpec(imageBlockConfig, { + render: (block, editor) => fileRender(block, editor), + parse: (element) => fileParse(element), + toExternalHTML: (block, editor) => fileToExternalHTML(block, editor), +}); diff --git a/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts b/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts index 32a51d2963..33712e8629 100644 --- a/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts +++ b/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts @@ -1,6 +1,6 @@ +import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockFromConfig, BlockSchemaWithBlock } from "../../../../schema"; import { fileBlockConfig } from "../../fileBlockConfig"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; export const renderWithResizeHandles = ( block: BlockFromConfig, @@ -112,7 +112,6 @@ export const renderWithResizeHandles = ( resizeParams = undefined; editor.updateBlock(block, { - type: "file", props: { previewWidth: getWidth(), }, diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockConfig.ts b/packages/core/src/blocks/FileBlockContent/fileBlockConfig.ts deleted file mode 100644 index 923b2a20c8..0000000000 --- a/packages/core/src/blocks/FileBlockContent/fileBlockConfig.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { defaultProps } from "../defaultProps"; -import { CustomBlockConfig, PropSchema } from "../../schema"; - -export const filePropSchema = { - textAlignment: defaultProps.textAlignment, - backgroundColor: defaultProps.backgroundColor, - // File type. - fileType: { - default: "" as const, - }, - // File name. - name: { - default: "" as const, - }, - // File url. - url: { - default: "" as const, - }, - // File caption. - caption: { - default: "" as const, - }, - // Whether to show the file preview or the name only. - showPreview: { - default: true as const, - }, - // File preview width in px. - previewWidth: { - default: 512 as const, - }, -} satisfies PropSchema; - -export const fileBlockConfig = { - type: "file" as const, - propSchema: filePropSchema, - content: "none", -} satisfies CustomBlockConfig; diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockExtension.ts b/packages/core/src/blocks/FileBlockContent/fileBlockExtension.ts deleted file mode 100644 index f9c4c2b0ec..0000000000 --- a/packages/core/src/blocks/FileBlockContent/fileBlockExtension.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - BlockFromConfig, - BlockSchemaWithBlock, - PartialBlockFromConfig, -} from "../../schema"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { fileBlockConfig } from "./fileBlockConfig"; - -export type FileBlockExtension = { - fileEndings: string[]; - render: ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - > - ) => { - dom: HTMLElement; - destroy?: () => void; - }; - parse?: ( - element: HTMLElement - ) => - | PartialBlockFromConfig["props"] - | undefined; - toExternalHTML?: ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - > - ) => { - dom: HTMLElement; - }; - buttonText?: string; - buttonIcon?: () => HTMLElement; -}; diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts new file mode 100644 index 0000000000..fb12f1f493 --- /dev/null +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -0,0 +1,227 @@ +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { BlockFromConfig, FileBlockConfig } from "../../schema"; + +export const createFileIconAndNameDOM = ( + block: BlockFromConfig +): { dom: HTMLElement; destroy?: () => void } => { + const file = document.createElement("div"); + file.className = "bn-file-default-preview"; + + const icon = document.createElement("div"); + icon.className = "bn-file-default-preview-icon"; + icon.innerHTML = + ''; + + const fileName = document.createElement("p"); + fileName.className = "bn-file-default-preview-name"; + fileName.innerText = block.props.name || ""; + + file.appendChild(icon); + file.appendChild(fileName); + + return { + dom: file, + }; +}; + +export const createFileAndCaptionDOM = ( + block: BlockFromConfig, + editor: BlockNoteEditor, + file: HTMLElement +) => { + // Wrapper element for the file, resize handles and caption. + const fileAndCaptionWrapper = document.createElement("div"); + fileAndCaptionWrapper.className = "bn-file-and-caption-wrapper"; + + // Caption element. + const caption = document.createElement("p"); + caption.className = "bn-file-caption"; + caption.innerText = block.props.caption; + + fileAndCaptionWrapper.appendChild(file); + fileAndCaptionWrapper.appendChild(caption); + + return { + dom: fileAndCaptionWrapper, + }; +}; + +export const createFilePlaceholderDOM = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + // Button element that acts as a placeholder for files with no src. + const addFileButton = document.createElement("div"); + addFileButton.className = "bn-add-file-button"; + + // Icon for the add file button. + const addFileButtonIcon = document.createElement("div"); + addFileButtonIcon.className = "bn-add-file-button-icon"; + + // Text for the add file button. + const addFileButtonText = document.createElement("p"); + addFileButtonText.className = "bn-add-file-button-text"; + addFileButtonText.innerHTML = + /*`${editor.dictionary.file.button_add_text} ${ + block.props.fileType && + extensions && + block.props.fileType in extensions && + extensions[block.props.fileType].buttonText !== undefined + ? extensions[block.props.fileType].buttonText! + : */ editor.dictionary.file.button_default_file_type_text; + // }`; + + // Prevents focus from moving to the button. + const addFileButtonMouseDownHandler = (event: MouseEvent) => { + event.preventDefault(); + }; + // Opens the file toolbar. + const addFileButtonClickHandler = () => { + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + block: block, + }) + ); + }; + + // if ( + // block.props.fileType && + // extensions && + // block.props.fileType in extensions && + // extensions[block.props.fileType].buttonIcon !== undefined + // ) { + // addFileButtonIcon.appendChild( + // extensions[block.props.fileType].buttonIcon!() + // ); + // } else { + addFileButtonIcon.innerHTML = + ''; + // } + addFileButton.appendChild(addFileButtonIcon); + addFileButton.appendChild(addFileButtonText); + + addFileButton.addEventListener( + "mousedown", + addFileButtonMouseDownHandler, + true + ); + addFileButton.addEventListener("click", addFileButtonClickHandler, true); + + return { + dom: addFileButton, + destroy: () => { + addFileButton.removeEventListener( + "mousedown", + addFileButtonMouseDownHandler, + true + ); + addFileButton.removeEventListener( + "click", + addFileButtonClickHandler, + true + ); + }, + }; +}; + +export const fileParse = (element: HTMLElement) => { + // Checks if any extensions can parse the element. + // const propsFromExtension = Object.values(parseExtensions || {}) + // .map((extension) => + // extension.parse ? extension.parse(element) : undefined + // ) + // .find((item) => item !== undefined); + + // if (propsFromExtension) { + // return propsFromExtension; + // } + + // Falls back to default parsing logic. + if (element.tagName === "EMBED") { + // const fileType = element.getAttribute("type"); + const url = element.getAttribute("src"); + const previewWidth = element.getAttribute("width"); + + return { + // fileType: + // fileType && parseExtensions && fileType in parseExtensions + // ? fileType.split("/")[0] + // : undefined, + url: url || undefined, + previewWidth: previewWidth ? parseInt(previewWidth) : undefined, + }; + } + + if (element.tagName === "FIGURE") { + const fileElement = element.querySelector("embed"); + const captionElement = element.querySelector("figcaption"); + + // const fileType = fileElement?.type; + const url = fileElement?.src; + const previewWidth = fileElement?.width; + const caption = captionElement?.textContent; + + return { + url: url || undefined, + previewWidth: previewWidth ? parseInt(previewWidth) : undefined, + caption: caption || undefined, + }; + } + + return undefined; +}; + +export const fileToExternalHTML = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + // if (!block.props.url) { + // const div = document.createElement("p"); + // div.innerHTML = `${editor.dictionary.file.button_add_text} ${ + // block.props.fileType && + // extensions && + // block.props.fileType in extensions && + // extensions[block.props.fileType].buttonText !== undefined + // ? extensions[block.props.fileType].buttonText + // : editor.dictionary.file.button_default_file_type_text + // }`; + + // return { + // dom: div, + // }; + // } + + // if ( + // extensions && + // block.props.fileType && + // block.props.fileType in extensions && + // extensions[block.props.fileType].toExternalHTML + // ) { + // return extensions[block.props.fileType].toExternalHTML!(block, editor); + // } + + // TBD: should default be of type "embed"? + const embed = document.createElement("embed"); + // if (block.props.fileType) { + // embed.type = block.props.fileType; + // } + embed.src = block.props.url; + + if (block.props.caption) { + const figure = document.createElement("figure"); + const caption = document.createElement("figcaption"); + + caption.textContent = block.props.caption; + + figure.appendChild(embed); + figure.appendChild(caption); + + return { + dom: figure, + }; + } + + return { + dom: embed, + }; +}; diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockImplementation.ts b/packages/core/src/blocks/FileBlockContent/fileBlockImplementation.ts deleted file mode 100644 index 23159f1a54..0000000000 --- a/packages/core/src/blocks/FileBlockContent/fileBlockImplementation.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { - BlockFromConfig, - BlockSchemaWithBlock, - CustomBlockImplementation, -} from "../../schema"; -import { fileBlockConfig } from "./fileBlockConfig"; -import { FileBlockExtension } from "./fileBlockExtension"; - -const defaultFileRender = ( - block: BlockFromConfig -): { dom: HTMLElement; destroy?: () => void } => { - const file = document.createElement("div"); - file.className = "bn-file-default-preview"; - - const icon = document.createElement("div"); - icon.className = "bn-file-default-preview-icon"; - icon.innerHTML = - ''; - - const fileName = document.createElement("p"); - fileName.className = "bn-file-default-preview-name"; - fileName.innerHTML = block.props.name || ""; - - file.appendChild(icon); - file.appendChild(fileName); - - return { - dom: file, - }; -}; - -export const fileRender = ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - >, - extensions?: Record< - string, - Pick - > -) => { - // Wrapper element to set the file alignment, contains both file/file - // upload dashboard and caption. - const wrapper = document.createElement("div"); - wrapper.className = "bn-file-block-content-wrapper"; - - // Button element that acts as a placeholder for files with no src. - const addFileButton = document.createElement("div"); - addFileButton.className = "bn-add-file-button"; - - // Icon for the add file button. - const addFileButtonIcon = document.createElement("div"); - addFileButtonIcon.className = "bn-add-file-button-icon"; - - // Text for the add file button. - const addFileButtonText = document.createElement("p"); - addFileButtonText.className = "bn-add-file-button-text"; - addFileButtonText.innerHTML = `${editor.dictionary.file.button_add_text} ${ - block.props.fileType && - extensions && - block.props.fileType in extensions && - extensions[block.props.fileType].buttonText !== undefined - ? extensions[block.props.fileType].buttonText! - : editor.dictionary.file.button_default_file_type_text - }`; - - // Wrapper element for the file, resize handles and caption. - const fileAndCaptionWrapper = document.createElement("div"); - fileAndCaptionWrapper.className = "bn-file-and-caption-wrapper"; - - const fileType = block.props.fileType; - const renderedFileExtension = - block.props.showPreview && fileType && extensions && fileType in extensions - ? extensions[fileType].render(block, editor) - : defaultFileRender(block); - - // File element. - const file = renderedFileExtension.dom; - - // Caption element. - const caption = document.createElement("p"); - caption.className = "bn-file-caption"; - caption.innerHTML = block.props.caption; - - // Prevents focus from moving to the button. - const addFileButtonMouseDownHandler = (event: MouseEvent) => { - event.preventDefault(); - }; - // Opens the file toolbar. - const addFileButtonClickHandler = () => { - editor._tiptapEditor.view.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - block: block, - }) - ); - }; - - if ( - block.props.fileType && - extensions && - block.props.fileType in extensions && - extensions[block.props.fileType].buttonIcon !== undefined - ) { - addFileButtonIcon.appendChild( - extensions[block.props.fileType].buttonIcon!() - ); - } else { - addFileButtonIcon.innerHTML = - ''; - } - addFileButton.appendChild(addFileButtonIcon); - addFileButton.appendChild(addFileButtonText); - - fileAndCaptionWrapper.appendChild(file); - fileAndCaptionWrapper.appendChild(caption); - - if (block.props.url === "") { - wrapper.appendChild(addFileButton); - } else { - wrapper.appendChild(fileAndCaptionWrapper); - } - - addFileButton.addEventListener( - "mousedown", - addFileButtonMouseDownHandler, - true - ); - addFileButton.addEventListener("click", addFileButtonClickHandler, true); - - return { - dom: wrapper, - destroy: () => { - addFileButton.removeEventListener( - "mousedown", - addFileButtonMouseDownHandler, - true - ); - addFileButton.removeEventListener( - "click", - addFileButtonClickHandler, - true - ); - renderedFileExtension?.destroy?.(); - }, - }; -}; - -export const fileParse = ( - element: HTMLElement, - parseExtensions?: Record> -) => { - // Checks if any extensions can parse the element. - const propsFromExtension = Object.values(parseExtensions || {}) - .map((extension) => - extension.parse ? extension.parse(element) : undefined - ) - .find((item) => item !== undefined); - - if (propsFromExtension) { - return propsFromExtension; - } - - // Falls back to default parsing logic. - if (element.tagName === "EMBED") { - const fileType = element.getAttribute("type"); - const url = element.getAttribute("src"); - const previewWidth = element.getAttribute("width"); - - return { - fileType: - fileType && parseExtensions && fileType in parseExtensions - ? fileType.split("/")[0] - : undefined, - url: url || undefined, - previewWidth: previewWidth ? parseInt(previewWidth) : undefined, - }; - } - - if (element.tagName === "FIGURE") { - const fileElement = element.querySelector("embed"); - const captionElement = element.querySelector("figcaption"); - - const fileType = fileElement?.type; - const url = fileElement?.src; - const previewWidth = fileElement?.width; - const caption = captionElement?.textContent; - - return { - fileType: - fileType && parseExtensions && fileType in parseExtensions - ? fileType.split("/")[0] - : undefined, - url: url || undefined, - previewWidth: previewWidth ? parseInt(previewWidth) : undefined, - caption: caption || undefined, - }; - } - - return undefined; -}; - -export const fileToExternalHTML = ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - >, - extensions?: Record< - string, - Pick - > -) => { - if (!block.props.url) { - const div = document.createElement("p"); - div.innerHTML = `${editor.dictionary.file.button_add_text} ${ - block.props.fileType && - extensions && - block.props.fileType in extensions && - extensions[block.props.fileType].buttonText !== undefined - ? extensions[block.props.fileType].buttonText - : editor.dictionary.file.button_default_file_type_text - }`; - - return { - dom: div, - }; - } - - if ( - extensions && - block.props.fileType && - block.props.fileType in extensions && - extensions[block.props.fileType].toExternalHTML - ) { - return extensions[block.props.fileType].toExternalHTML!(block, editor); - } - - const embed = document.createElement("embed"); - if (block.props.fileType) { - embed.type = block.props.fileType; - } - embed.src = block.props.url; - - if (block.props.caption) { - const figure = document.createElement("figure"); - const caption = document.createElement("figcaption"); - - caption.textContent = block.props.caption; - - figure.appendChild(embed); - figure.appendChild(caption); - - return { - dom: figure, - }; - } - - return { - dom: embed, - }; -}; - -export const createFileBlockImplementation = ( - extensions?: Record -) => - ({ - render: (block, editor) => fileRender(block, editor, extensions), - parse: (element) => fileParse(element, extensions), - toExternalHTML: (block, editor) => - fileToExternalHTML(block, editor, extensions), - extensions: extensions || {}, - } satisfies CustomBlockImplementation & { - extensions: Record; - }); diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index a2629b2488..29064a1513 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -19,12 +19,12 @@ import { getInlineContentSchemaFromSpecs, getStyleSchemaFromSpecs, } from "../schema"; +import { FileBlock } from "./FileBlockContent/FileBlockContent"; +import { ImageBlock } from "./FileBlockContent/ImageBlockContent"; import { Heading } from "./HeadingBlockContent/HeadingBlockContent"; import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { Paragraph } from "./ParagraphBlockContent/ParagraphBlockContent"; -import { createFileBlock } from "./FileBlockContent/FileBlockContent"; -import { defaultFileExtensions } from "./FileBlockContent/extensions/defaultFileExtensions"; import { Table } from "./TableBlockContent/TableBlockContent"; export const defaultBlockSpecs = { @@ -32,7 +32,8 @@ export const defaultBlockSpecs = { heading: Heading, bulletListItem: BulletListItem, numberedListItem: NumberedListItem, - file: createFileBlock(defaultFileExtensions), + file: FileBlock, + image: ImageBlock, table: Table, } satisfies BlockSpecs; diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 98cbe0f71c..c9a9d5a46c 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -237,11 +237,8 @@ NESTED BLOCKS /* FILES */ -[data-content-type="file"] { +.bn-file-block-content-wrapper { cursor: pointer; -} - -[data-content-type="file"] .bn-file-block-content-wrapper { display: flex; flex-direction: column; justify-content: stretch; @@ -249,11 +246,12 @@ NESTED BLOCKS width: 100%; } -[data-content-type="file"]:not([data-url]) { +/* // TODO */ +.bn-file-block-content-wrapper:not([data-url]) { width: 100%; } -[data-content-type="file"] .bn-add-file-button { +.bn-file-block-content-wrapper .bn-add-file-button { display: flex; flex-direction: row; align-items: center; @@ -265,42 +263,42 @@ NESTED BLOCKS width: 100%; } -[data-content-type="file"] .bn-add-file-button:hover { +.bn-file-block-content-wrapper .bn-add-file-button:hover { background-color: gainsboro; } -[data-content-type="file"] .bn-add-file-button-icon { +.bn-file-block-content-wrapper .bn-add-file-button-icon { width: 24px; height: 24px; } -[data-content-type="file"] .bn-add-file-button-text { +.bn-file-block-content-wrapper .bn-add-file-button-text { color: black; } -[data-content-type="file"] .bn-file-and-caption-wrapper { +.bn-file-block-content-wrapper .bn-file-and-caption-wrapper { display: flex; flex-direction: column; border-radius: 4px; } -[data-content-type="file"] .bn-file-default-preview { +.bn-file-block-content-wrapper .bn-file-default-preview { align-items: center; display: flex; flex-direction: row; gap: 4px; } -[data-content-type="file"] .bn-file-default-preview-icon { +.bn-file-block-content-wrapper .bn-file-default-preview-icon { width: 24px; height: 24px; } -[data-content-type="file"] .bn-file-default-preview-name { +.bn-file-block-content-wrapper .bn-file-default-preview-name { /*font-size: 0.8em;*/ } -[data-content-type="file"] .bn-visual-media-wrapper { +.bn-file-block-content-wrapper .bn-visual-media-wrapper { display: flex; flex-direction: row; align-items: center; @@ -308,12 +306,12 @@ NESTED BLOCKS width: fit-content; } -[data-content-type="file"] .bn-visual-media { +.bn-file-block-content-wrapper .bn-visual-media { border-radius: 4px; max-width: 100%; } -[data-content-type="file"] .bn-visual-media-resize-handle { +.bn-file-block-content-wrapper .bn-visual-media-resize-handle { position: absolute; width: 8px; height: 30px; @@ -323,16 +321,16 @@ NESTED BLOCKS cursor: ew-resize; } -[data-content-type="file"][data-file-type="audio"], [data-content-type="file"] .bn-audio { +.bn-file-block-content-wrapper .bn-audio { width: 100%; } -[data-content-type="file"] .bn-file-caption { +.bn-file-block-content-wrapper .bn-file-caption { font-size: 0.8em; padding-block: 4px; } -[data-content-type="file"] .bn-file-caption:empty { +.bn-file-block-content-wrapper .bn-file-caption:empty { padding-block: 0; } diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index fb4a254554..899a85e188 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -181,14 +181,11 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("file", editor)) { + if (checkDefaultBlockTypeInSchema("image", editor)) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { - type: "file", - props: { - fileType: "image", - }, + type: "image", }); // Immediately open the file toolbar @@ -201,44 +198,9 @@ export function getDefaultSlashMenuItems< key: "image", ...editor.dictionary.slash_menu.image, }); - items.push({ - onItemClick: () => { - const insertedBlock = insertOrUpdateBlock(editor, { - type: "file", - props: { - fileType: "video", - }, - }); - - // Immediately open the file toolbar - editor.prosemirrorView.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - block: insertedBlock, - }) - ); - }, - key: "video", - ...editor.dictionary.slash_menu.video, - }); - items.push({ - onItemClick: () => { - const insertedBlock = insertOrUpdateBlock(editor, { - type: "file", - props: { - fileType: "audio", - }, - }); + } - // Immediately open the file toolbar - editor.prosemirrorView.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - block: insertedBlock, - }) - ); - }, - key: "audio", - ...editor.dictionary.slash_menu.audio, - }); + if (checkDefaultBlockTypeInSchema("file", editor)) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -252,10 +214,65 @@ export function getDefaultSlashMenuItems< }) ); }, - key: "file", + key: "image", ...editor.dictionary.slash_menu.file, }); } + // items.push({ + // onItemClick: () => { + // const insertedBlock = insertOrUpdateBlock(editor, { + // type: "file", + // props: { + // fileType: "video", + // }, + // }); + + // // Immediately open the file toolbar + // editor.prosemirrorView.dispatch( + // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + // block: insertedBlock, + // }) + // ); + // }, + // key: "video", + // ...editor.dictionary.slash_menu.video, + // }); + // items.push({ + // onItemClick: () => { + // const insertedBlock = insertOrUpdateBlock(editor, { + // type: "file", + // props: { + // fileType: "audio", + // }, + // }); + + // // Immediately open the file toolbar + // editor.prosemirrorView.dispatch( + // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + // block: insertedBlock, + // }) + // ); + // }, + // key: "audio", + // ...editor.dictionary.slash_menu.audio, + // }); + // items.push({ + // onItemClick: () => { + // const insertedBlock = insertOrUpdateBlock(editor, { + // type: "file", + // }); + + // // Immediately open the file toolbar + // editor.prosemirrorView.dispatch( + // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + // block: insertedBlock, + // }) + // ); + // }, + // key: "file", + // ...editor.dictionary.slash_menu.file, + // }); + // } return items; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bc7a6323cc..75c3936af2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,15 +2,14 @@ import * as locales from "./i18n/locales"; export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; -export * from "./blocks/FileBlockContent/fileBlockConfig"; -export * from "./blocks/FileBlockContent/fileBlockImplementation"; -export * from "./blocks/FileBlockContent/fileBlockExtension"; export * from "./blocks/FileBlockContent/FileBlockContent"; -export * from "./blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; -export * from "./blocks/FileBlockContent/extensions/imageFileExtension"; -export * from "./blocks/FileBlockContent/extensions/videoFileExtension"; export * from "./blocks/FileBlockContent/extensions/audioFileExtension"; export * from "./blocks/FileBlockContent/extensions/defaultFileExtensions"; +export * from "./blocks/FileBlockContent/extensions/imageFileExtension"; +export * from "./blocks/FileBlockContent/extensions/videoFileExtension"; + +export * from "./blocks/FileBlockContent/fileBlockHelpers"; +export * from "./blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; export * from "./blocks/defaultBlockTypeGuards"; export * from "./blocks/defaultBlocks"; export * from "./blocks/defaultProps"; @@ -19,8 +18,8 @@ export * from "./editor/BlockNoteExtensions"; export * from "./editor/BlockNoteSchema"; export * from "./editor/selectionTypes"; export * from "./extensions-shared/UiElementPosition"; -export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; export * from "./extensions/FilePanel/FilePanelPlugin"; +export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; export * from "./extensions/LinkToolbar/LinkToolbarPlugin"; export * from "./extensions/SideMenu/SideMenuPlugin"; export * from "./extensions/SuggestionMenu/DefaultSuggestionItem"; diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 66d073655d..56a06ca34a 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -21,14 +21,42 @@ export type BlockNoteDOMAttributes = Partial<{ [DOMElement in BlockNoteDOMElement]: Record; }>; -// BlockConfig contains the "schema" info about a Block type -// i.e. what props it supports, what content it supports, etc. -export type BlockConfig = { +export type FileBlockConfig = { type: string; - readonly propSchema: PropSchema; - content: "inline" | "none" | "table"; + readonly propSchema: PropSchema & { + url: { + default: ""; + }; + caption: { + default: ""; + }; + name: { + default: ""; + }; + // Whether to show the file preview or the name only. + showPreview?: { + default: boolean; + }; + // File preview width in px. + previewWidth?: { + default: number; + }; + }; + content: "none"; + isFileBlock: true; }; +// BlockConfig contains the "schema" info about a Block type +// i.e. what props it supports, what content it supports, etc. +export type BlockConfig = + | { + type: string; + readonly propSchema: PropSchema; + content: "inline" | "none" | "table"; + isFileBlock?: false; + } + | FileBlockConfig; + // Block implementation contains the "implementation" info about a Block // such as the functions / Nodes required to render and / or serialize it export type TiptapBlockImplementation< diff --git a/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx b/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx index 1def16a59c..81180c2f61 100644 --- a/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx +++ b/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx @@ -91,7 +91,6 @@ export const ResizeHandlesWrapper = < setResizeParams(undefined); props.editor.updateBlock(props.block, { - type: "file", props: { previewWidth: props.width, }, diff --git a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx index 623a6fc25b..eb82ec3d7c 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx @@ -43,7 +43,6 @@ export const EmbedTab = < if (event.key === "Enter") { event.preventDefault(); editor.updateBlock(block, { - type: "file", props: { name: currentURL.split("/")[-1], url: currentURL, @@ -56,7 +55,6 @@ export const EmbedTab = < const handleURLClick = useCallback(() => { editor.updateBlock(block, { - type: "file", props: { name: currentURL.split("/")[-1], url: currentURL, diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx index 06d02a95c0..d5a0ede539 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx @@ -54,10 +54,8 @@ export const UploadTab = < try { const uploaded = await editor.uploadFile(file); editor.updateBlock(block, { - type: "file", props: { // TODO: Get type from file extension, not MIME type - fileType: file.type.split("/")[0], name: file.name, url: uploaded, }, @@ -80,7 +78,8 @@ export const UploadTab = < Date: Fri, 17 May 2024 13:56:19 +0200 Subject: [PATCH 2/7] update formatting toolbar buttons --- .../FileBlockContent/ImageBlockContent.ts | 1 - .../core/src/blocks/defaultBlockTypeGuards.ts | 45 ++++++++++++++- .../getDefaultSlashMenuItems.ts | 55 ------------------- .../DefaultButtons/FileCaptionButton.tsx | 13 ++--- .../DefaultButtons/FileDeleteButton.tsx | 4 +- .../DefaultButtons/FileDownloadButton.tsx | 4 +- .../DefaultButtons/FilePreviewButton.tsx | 21 ++----- .../DefaultButtons/FileRenameButton.tsx | 13 ++--- 8 files changed, 61 insertions(+), 95 deletions(-) diff --git a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts index e7cccf68ed..5a6deca9cc 100644 --- a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts @@ -64,7 +64,6 @@ export const fileRender = ( // File element. - debugger; if (block.props.url === "") { const placeholder = createFilePlaceholderDOM(block, editor); wrapper.appendChild(placeholder.dom); diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 175aabca2b..dad1e461f1 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -1,5 +1,11 @@ import type { BlockNoteEditor } from "../editor/BlockNoteEditor"; -import { BlockFromConfig, InlineContentSchema, StyleSchema } from "../schema"; +import { + BlockFromConfig, + BlockSchema, + FileBlockConfig, + InlineContentSchema, + StyleSchema, +} from "../schema"; import { Block, DefaultBlockSchema, defaultBlockSchema } from "./defaultBlocks"; import { defaultProps } from "./defaultProps"; @@ -33,6 +39,43 @@ export function checkBlockIsDefaultType< ); } +export function checkBlockIsFileBlock< + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + block: Block, + editor: BlockNoteEditor +): block is BlockFromConfig { + return ( + (block.type in editor.schema.blockSchema && + editor.schema.blockSchema[block.type].isFileBlock) || + false + ); +} + +export function checkBlockIsFileBlockWithPreview< + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + block: Block, + editor: BlockNoteEditor +): block is BlockFromConfig< + FileBlockConfig & { + propSchema: Required; + }, + I, + S +> { + return ( + (block.type in editor.schema.blockSchema && + editor.schema.blockSchema[block.type].isFileBlock && + "showPreview" in editor.schema.blockSchema[block.type].propSchema) || + false + ); +} + export function checkBlockTypeHasDefaultProp< Prop extends keyof typeof defaultProps, I extends InlineContentSchema, diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 899a85e188..8ef05e7fb1 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -218,61 +218,6 @@ export function getDefaultSlashMenuItems< ...editor.dictionary.slash_menu.file, }); } - // items.push({ - // onItemClick: () => { - // const insertedBlock = insertOrUpdateBlock(editor, { - // type: "file", - // props: { - // fileType: "video", - // }, - // }); - - // // Immediately open the file toolbar - // editor.prosemirrorView.dispatch( - // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - // block: insertedBlock, - // }) - // ); - // }, - // key: "video", - // ...editor.dictionary.slash_menu.video, - // }); - // items.push({ - // onItemClick: () => { - // const insertedBlock = insertOrUpdateBlock(editor, { - // type: "file", - // props: { - // fileType: "audio", - // }, - // }); - - // // Immediately open the file toolbar - // editor.prosemirrorView.dispatch( - // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - // block: insertedBlock, - // }) - // ); - // }, - // key: "audio", - // ...editor.dictionary.slash_menu.audio, - // }); - // items.push({ - // onItemClick: () => { - // const insertedBlock = insertOrUpdateBlock(editor, { - // type: "file", - // }); - - // // Immediately open the file toolbar - // editor.prosemirrorView.dispatch( - // editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { - // block: insertedBlock, - // }) - // ); - // }, - // key: "file", - // ...editor.dictionary.slash_menu.file, - // }); - // } return items; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx index 64f084baca..eb0cb192ff 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx @@ -1,7 +1,6 @@ import { BlockSchema, - checkBlockIsDefaultType, - checkDefaultBlockTypeInSchema, + checkBlockIsFileBlock, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +40,7 @@ export const FileCaptionButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsDefaultType("file", block, editor)) { + if (checkBlockIsFileBlock(block, editor)) { setCurrentEditingCaption(block.props.caption); return block; } @@ -51,16 +50,12 @@ export const FileCaptionButton = () => { const handleEnter = useCallback( (event: KeyboardEvent) => { - if ( - fileBlock && - checkDefaultBlockTypeInSchema("file", editor) && - event.key === "Enter" - ) { + if (fileBlock && event.key === "Enter") { event.preventDefault(); editor.updateBlock(fileBlock, { type: "file", props: { - caption: currentEditingCaption, + caption: currentEditingCaption as any, // TODO }, }); } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx index 7009bc12b0..91485ef6ef 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx @@ -1,6 +1,6 @@ import { BlockSchema, - checkBlockIsDefaultType, + checkBlockIsFileBlock, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -32,7 +32,7 @@ export const FileDeleteButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsDefaultType("file", block, editor)) { + if (checkBlockIsFileBlock(block, editor)) { return block; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx index 76dbad4b69..aa1f9128d6 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx @@ -1,6 +1,6 @@ import { BlockSchema, - checkBlockIsDefaultType, + checkBlockIsFileBlock, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -32,7 +32,7 @@ export const FileDownloadButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsDefaultType("file", block, editor)) { + if (checkBlockIsFileBlock(block, editor)) { return block; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx index e5999cbd6d..c88f417478 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx @@ -1,7 +1,6 @@ import { BlockSchema, - checkBlockIsDefaultType, - checkDefaultBlockTypeInSchema, + checkBlockIsFileBlockWithPreview, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -33,34 +32,24 @@ export const FilePreviewButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsDefaultType("file", block, editor)) { + if (checkBlockIsFileBlockWithPreview(block, editor)) { return block; } return undefined; }, [editor, selectedBlocks]); - // TODO: Where can we store the extensions such that we can access them here? - const fileBlockHasPreview = useMemo(() => { - return ( - fileBlock?.props.fileType === "image" || - fileBlock?.props.fileType === "audio" || - fileBlock?.props.fileType === "video" - ); - }, [fileBlock]); - const onClick = useCallback(() => { - if (fileBlock && checkDefaultBlockTypeInSchema("file", editor)) { + if (fileBlock) { editor.updateBlock(fileBlock, { - type: "file", props: { - showPreview: !fileBlock.props.showPreview, + showPreview: !fileBlock.props.showPreview as any, // TODO }, }); } }, [editor, fileBlock]); - if (!fileBlock || !fileBlock.props.url || !fileBlockHasPreview) { + if (!fileBlock || !fileBlock.props.url) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx index 6003604871..b95f38335d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx @@ -1,7 +1,6 @@ import { BlockSchema, - checkBlockIsDefaultType, - checkDefaultBlockTypeInSchema, + checkBlockIsFileBlock, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +40,7 @@ export const FileRenameButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsDefaultType("file", block, editor)) { + if (checkBlockIsFileBlock(block, editor)) { setCurrentEditingName(block.props.name); return block; } @@ -51,16 +50,12 @@ export const FileRenameButton = () => { const handleEnter = useCallback( (event: KeyboardEvent) => { - if ( - fileBlock && - checkDefaultBlockTypeInSchema("file", editor) && - event.key === "Enter" - ) { + if (fileBlock && event.key === "Enter") { event.preventDefault(); editor.updateBlock(fileBlock, { type: "file", props: { - name: currentEditingName, + name: currentEditingName as any, // TODO }, }); } From a6cac7ac73b46dacef3fc9c97f2fbae95ea58697 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 17 May 2024 14:49:57 +0200 Subject: [PATCH 3/7] Added `parse` & `toExternalHTML` --- .../FileBlockContent/FileBlockContent.ts | 6 +- .../FileBlockContent/ImageBlockContent.ts | 66 +++++++++- .../FileBlockContent/fileBlockHelpers.ts | 123 ++++++++---------- packages/core/src/schema/blocks/types.ts | 1 + 4 files changed, 116 insertions(+), 80 deletions(-) diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index 235383a378..05b22f93cd 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -77,9 +77,9 @@ export const fileRender = ( }; export const FileBlock = createBlockSpec(fileBlockConfig, { - render: (block, editor) => fileRender(block, editor), - parse: (element) => fileParse(element), - toExternalHTML: (block, editor) => fileToExternalHTML(block, editor), + render: fileRender, + parse: fileParse as any, // TODO: See FileBlockConfig type + toExternalHTML: fileToExternalHTML, }); // - React support? diff --git a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts index 5a6deca9cc..0c48090403 100644 --- a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts @@ -4,6 +4,7 @@ import { FileBlockConfig, PropSchema, createBlockSpec, + Props, } from "../../schema"; import { defaultProps } from "../defaultProps"; import { renderWithResizeHandles } from "./extensions/utils/renderWithResizeHandles"; @@ -12,8 +13,8 @@ import { createFileAndCaptionDOM, createFileIconAndNameDOM, createFilePlaceholderDOM, - fileParse, - fileToExternalHTML, + parseFigure, + toExternalHTMLWithCaption, } from "./fileBlockHelpers"; export const propSchema = { @@ -111,8 +112,63 @@ export const fileRender = ( } }; +export const parseImage = (imageElement: HTMLImageElement) => { + const url = imageElement.src || undefined; + const previewWidth = imageElement.width || undefined; + + return { url, previewWidth }; +}; + +export const imageParse = ( + element: HTMLElement +): Partial> | undefined => { + if (element.tagName === "IMG") { + return parseImage(element as HTMLImageElement); + } + + if (element.tagName === "FIGURE") { + const parsedFigure = parseFigure(element, "img"); + if (!parsedFigure) { + return undefined; + } + + const { targetElement, caption } = parsedFigure; + + return { + ...parseImage(targetElement as HTMLImageElement), + caption, + }; + } + + return undefined; +}; + +export const imageToExternalHTML = ( + block: BlockFromConfig +) => { + if (!block.props.url) { + const div = document.createElement("p"); + div.innerHTML = "Add image"; + + return { + dom: div, + }; + } + + const image = document.createElement("img"); + image.src = block.props.url; + + if (block.props.caption) { + return toExternalHTMLWithCaption(image, block.props.caption); + } + + return { + dom: image, + }; +}; + export const ImageBlock = createBlockSpec(imageBlockConfig, { - render: (block, editor) => fileRender(block, editor), - parse: (element) => fileParse(element), - toExternalHTML: (block, editor) => fileToExternalHTML(block, editor), + render: fileRender, + parse: imageParse, + toExternalHTML: imageToExternalHTML, }); diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts index fb12f1f493..f162b7bcc5 100644 --- a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -124,101 +124,80 @@ export const createFilePlaceholderDOM = ( }; }; -export const fileParse = (element: HTMLElement) => { - // Checks if any extensions can parse the element. - // const propsFromExtension = Object.values(parseExtensions || {}) - // .map((extension) => - // extension.parse ? extension.parse(element) : undefined - // ) - // .find((item) => item !== undefined); - - // if (propsFromExtension) { - // return propsFromExtension; - // } +export const parseEmbed = (embedElement: HTMLEmbedElement) => { + const url = embedElement.src || undefined; - // Falls back to default parsing logic. - if (element.tagName === "EMBED") { - // const fileType = element.getAttribute("type"); - const url = element.getAttribute("src"); - const previewWidth = element.getAttribute("width"); + return { url }; +}; - return { - // fileType: - // fileType && parseExtensions && fileType in parseExtensions - // ? fileType.split("/")[0] - // : undefined, - url: url || undefined, - previewWidth: previewWidth ? parseInt(previewWidth) : undefined, - }; +export const parseFigure = (figureElement: HTMLElement, targetTag: string) => { + const targetElement = figureElement.querySelector( + targetTag + ) as HTMLElement | null; + if (!targetElement) { + return undefined; + } + + const captionElement = figureElement.querySelector("figcaption"); + const caption = captionElement?.textContent ?? undefined; + + return { targetElement, caption }; +}; + +export const fileParse = (element: HTMLElement) => { + if (element.tagName === "EMBED") { + return parseEmbed(element as HTMLEmbedElement); } if (element.tagName === "FIGURE") { - const fileElement = element.querySelector("embed"); - const captionElement = element.querySelector("figcaption"); + const parsedFigure = parseFigure(element, "embed"); + if (!parsedFigure) { + return undefined; + } - // const fileType = fileElement?.type; - const url = fileElement?.src; - const previewWidth = fileElement?.width; - const caption = captionElement?.textContent; + const { targetElement, caption } = parsedFigure; return { - url: url || undefined, - previewWidth: previewWidth ? parseInt(previewWidth) : undefined, - caption: caption || undefined, + ...parseEmbed(targetElement as HTMLEmbedElement), + caption, }; } return undefined; }; +export const toExternalHTMLWithCaption = ( + element: HTMLElement, + caption: string +) => { + const figure = document.createElement("figure"); + const captionElement = document.createElement("figcaption"); + captionElement.textContent = caption; + + figure.appendChild(element); + figure.appendChild(captionElement); + + return { dom: figure }; +}; + export const fileToExternalHTML = ( - block: BlockFromConfig, - editor: BlockNoteEditor + block: BlockFromConfig ) => { - // if (!block.props.url) { - // const div = document.createElement("p"); - // div.innerHTML = `${editor.dictionary.file.button_add_text} ${ - // block.props.fileType && - // extensions && - // block.props.fileType in extensions && - // extensions[block.props.fileType].buttonText !== undefined - // ? extensions[block.props.fileType].buttonText - // : editor.dictionary.file.button_default_file_type_text - // }`; - - // return { - // dom: div, - // }; - // } + if (!block.props.url) { + const div = document.createElement("p"); + div.innerHTML = "Add file"; - // if ( - // extensions && - // block.props.fileType && - // block.props.fileType in extensions && - // extensions[block.props.fileType].toExternalHTML - // ) { - // return extensions[block.props.fileType].toExternalHTML!(block, editor); - // } + return { + dom: div, + }; + } // TBD: should default be of type "embed"? const embed = document.createElement("embed"); - // if (block.props.fileType) { - // embed.type = block.props.fileType; - // } embed.src = block.props.url; if (block.props.caption) { - const figure = document.createElement("figure"); - const caption = document.createElement("figcaption"); - - caption.textContent = block.props.caption; - - figure.appendChild(embed); - figure.appendChild(caption); - - return { - dom: figure, - }; + return toExternalHTMLWithCaption(embed, block.props.caption); } return { diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 56a06ca34a..f7962d5e7e 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -23,6 +23,7 @@ export type BlockNoteDOMAttributes = Partial<{ export type FileBlockConfig = { type: string; + // TODO: The `PropSchema & ` breaks typing for `parse` function readonly propSchema: PropSchema & { url: { default: ""; From 3510762ad1af9a03bdad88104d7624418fca8380 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 17 May 2024 14:56:07 +0200 Subject: [PATCH 4/7] Moved `parse` & `toExternalHTML` for file block --- .../FileBlockContent/FileBlockContent.ts | 51 ++++++++++++++++++- .../FileBlockContent/fileBlockHelpers.ts | 47 ----------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index 05b22f93cd..2e2eb0646a 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -11,8 +11,9 @@ import { createFileAndCaptionDOM, createFileIconAndNameDOM, createFilePlaceholderDOM, - fileParse, - fileToExternalHTML, + parseEmbed, + parseFigure, + toExternalHTMLWithCaption, } from "./fileBlockHelpers"; export const filePropSchema = { @@ -76,6 +77,52 @@ export const fileRender = ( } }; +export const fileParse = (element: HTMLElement) => { + if (element.tagName === "EMBED") { + return parseEmbed(element as HTMLEmbedElement); + } + + if (element.tagName === "FIGURE") { + const parsedFigure = parseFigure(element, "embed"); + if (!parsedFigure) { + return undefined; + } + + const { targetElement, caption } = parsedFigure; + + return { + ...parseEmbed(targetElement as HTMLEmbedElement), + caption, + }; + } + + return undefined; +}; +export const fileToExternalHTML = ( + block: BlockFromConfig +) => { + if (!block.props.url) { + const div = document.createElement("p"); + div.innerHTML = "Add file"; + + return { + dom: div, + }; + } + + // TBD: should default be of type "embed"? + const embed = document.createElement("embed"); + embed.src = block.props.url; + + if (block.props.caption) { + return toExternalHTMLWithCaption(embed, block.props.caption); + } + + return { + dom: embed, + }; +}; + export const FileBlock = createBlockSpec(fileBlockConfig, { render: fileRender, parse: fileParse as any, // TODO: See FileBlockConfig type diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts index f162b7bcc5..7768a3e60d 100644 --- a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -144,28 +144,6 @@ export const parseFigure = (figureElement: HTMLElement, targetTag: string) => { return { targetElement, caption }; }; -export const fileParse = (element: HTMLElement) => { - if (element.tagName === "EMBED") { - return parseEmbed(element as HTMLEmbedElement); - } - - if (element.tagName === "FIGURE") { - const parsedFigure = parseFigure(element, "embed"); - if (!parsedFigure) { - return undefined; - } - - const { targetElement, caption } = parsedFigure; - - return { - ...parseEmbed(targetElement as HTMLEmbedElement), - caption, - }; - } - - return undefined; -}; - export const toExternalHTMLWithCaption = ( element: HTMLElement, caption: string @@ -179,28 +157,3 @@ export const toExternalHTMLWithCaption = ( return { dom: figure }; }; - -export const fileToExternalHTML = ( - block: BlockFromConfig -) => { - if (!block.props.url) { - const div = document.createElement("p"); - div.innerHTML = "Add file"; - - return { - dom: div, - }; - } - - // TBD: should default be of type "embed"? - const embed = document.createElement("embed"); - embed.src = block.props.url; - - if (block.props.caption) { - return toExternalHTMLWithCaption(embed, block.props.caption); - } - - return { - dom: embed, - }; -}; From 21905d963fa20abdc8a8c8fa732b745e6009ac6f Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 17 May 2024 15:33:34 +0200 Subject: [PATCH 5/7] wip --- examples/01-basic/testing/App.tsx | 3 +- .../FileBlockContent/FileBlockContent.ts | 7 +- .../FileBlockContent/ImageBlockContent.ts | 8 +- .../FileBlockContent/fileBlockHelpers.ts | 4 +- packages/core/src/i18n/locales/en.ts | 3 +- packages/core/src/index.ts | 5 +- packages/core/src/schema/blocks/types.ts | 1 + .../components/FileBlock/FileBlockContent.tsx | 148 ++---------------- .../FileBlock/ImageBlockContent.tsx | 69 ++++++++ .../extensions/defaultReactFileExtensions.ts | 13 -- .../extensions/utils/ResizeHandlesWrapper.tsx | 7 +- .../components/FileBlock/fileBlockHelpers.tsx | 76 +++++++++ .../FileBlock/reactFileBlockExtension.tsx | 26 --- packages/react/src/index.ts | 4 +- 14 files changed, 180 insertions(+), 194 deletions(-) create mode 100644 packages/react/src/components/FileBlock/ImageBlockContent.tsx delete mode 100644 packages/react/src/components/FileBlock/extensions/defaultReactFileExtensions.ts create mode 100644 packages/react/src/components/FileBlock/fileBlockHelpers.tsx delete mode 100644 packages/react/src/components/FileBlock/reactFileBlockExtension.tsx diff --git a/examples/01-basic/testing/App.tsx b/examples/01-basic/testing/App.tsx index 4045053138..c00a167616 100644 --- a/examples/01-basic/testing/App.tsx +++ b/examples/01-basic/testing/App.tsx @@ -6,11 +6,12 @@ import { import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -import { useCreateBlockNote } from "@blocknote/react"; +import { ReactImageBlock, useCreateBlockNote } from "@blocknote/react"; const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, + image: ReactImageBlock, }, }); diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index 2e2eb0646a..5d7c4f88f0 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -41,7 +41,7 @@ export const fileBlockConfig = { } satisfies FileBlockConfig; export const fileRender = ( - block: BlockFromConfig, + block: BlockFromConfig, editor: BlockNoteEditor ) => { // Wrapper element to set the file alignment, contains both file/file @@ -98,8 +98,9 @@ export const fileParse = (element: HTMLElement) => { return undefined; }; + export const fileToExternalHTML = ( - block: BlockFromConfig + block: BlockFromConfig ) => { if (!block.props.url) { const div = document.createElement("p"); @@ -125,7 +126,7 @@ export const fileToExternalHTML = ( export const FileBlock = createBlockSpec(fileBlockConfig, { render: fileRender, - parse: fileParse as any, // TODO: See FileBlockConfig type + parse: fileParse, toExternalHTML: fileToExternalHTML, }); diff --git a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts index 0c48090403..97960253e0 100644 --- a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts @@ -3,8 +3,8 @@ import { BlockFromConfig, FileBlockConfig, PropSchema, - createBlockSpec, Props, + createBlockSpec, } from "../../schema"; import { defaultProps } from "../defaultProps"; import { renderWithResizeHandles } from "./extensions/utils/renderWithResizeHandles"; @@ -58,14 +58,10 @@ export const fileRender = ( const wrapper = document.createElement("div"); wrapper.className = "bn-file-block-content-wrapper"; - // const fileType = block.props.fileType; - // block.props.showPreview && fileType && extensions && fileType in extensions - // ? extensions[fileType].render(block, editor) - // : defaultFileRender(block); - // File element. if (block.props.url === "") { + // TODO: pass image related things const placeholder = createFilePlaceholderDOM(block, editor); wrapper.appendChild(placeholder.dom); diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts index 7768a3e60d..9f659275d4 100644 --- a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -46,6 +46,7 @@ export const createFileAndCaptionDOM = ( }; }; +// TODO: allow icon / text to be passed in export const createFilePlaceholderDOM = ( block: BlockFromConfig, editor: BlockNoteEditor @@ -61,6 +62,7 @@ export const createFilePlaceholderDOM = ( // Text for the add file button. const addFileButtonText = document.createElement("p"); addFileButtonText.className = "bn-add-file-button-text"; + addFileButtonText.innerHTML = /*`${editor.dictionary.file.button_add_text} ${ block.props.fileType && @@ -68,7 +70,7 @@ export const createFilePlaceholderDOM = ( block.props.fileType in extensions && extensions[block.props.fileType].buttonText !== undefined ? extensions[block.props.fileType].buttonText! - : */ editor.dictionary.file.button_default_file_type_text; + : */ editor.dictionary.file.button_add_file_text; // }`; // Prevents focus from moving to the button. diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 511f973c1b..46602aee2a 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -104,8 +104,7 @@ export const en = { numberedListItem: "List", }, file: { - button_add_text: "Add", - button_default_file_type_text: "file", + button_add_file_text: "Add file", }, // from react package: side_menu: { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 75c3936af2..798e49a79f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,10 +3,7 @@ export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; export * from "./blocks/FileBlockContent/FileBlockContent"; -export * from "./blocks/FileBlockContent/extensions/audioFileExtension"; -export * from "./blocks/FileBlockContent/extensions/defaultFileExtensions"; -export * from "./blocks/FileBlockContent/extensions/imageFileExtension"; -export * from "./blocks/FileBlockContent/extensions/videoFileExtension"; +export * from "./blocks/FileBlockContent/ImageBlockContent"; export * from "./blocks/FileBlockContent/fileBlockHelpers"; export * from "./blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index f7962d5e7e..b2f2881216 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -45,6 +45,7 @@ export type FileBlockConfig = { }; content: "none"; isFileBlock: true; + // TODO: add "accept" mime types here }; // BlockConfig contains the "schema" info about a Block type diff --git a/packages/react/src/components/FileBlock/FileBlockContent.tsx b/packages/react/src/components/FileBlock/FileBlockContent.tsx index c254c59fd5..84ae9310b5 100644 --- a/packages/react/src/components/FileBlock/FileBlockContent.tsx +++ b/packages/react/src/components/FileBlock/FileBlockContent.tsx @@ -8,16 +8,10 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { FC, useCallback, useMemo } from "react"; import { RiFile2Line } from "react-icons/ri"; -import { - createReactBlockSpec, - ReactCustomBlockImplementation, - ReactCustomBlockRenderProps, -} from "../../schema/ReactBlockSpec"; -import { defaultReactFileExtensions } from "./extensions/defaultReactFileExtensions"; -import { ReactFileBlockExtension } from "./reactFileBlockExtension"; +import { createReactBlockSpec } from "../../schema/ReactBlockSpec"; +import { FileAndCaption, FileAndPlaceholder } from "./fileBlockHelpers"; export const DefaultFileRender = < ISchema extends InlineContentSchema, @@ -39,101 +33,6 @@ export const DefaultFileRender = < ); -export const FileRender = < - ISchema extends InlineContentSchema, - SSchema extends StyleSchema ->( - props: Omit< - ReactCustomBlockRenderProps, - "contentRef" - > & { - extensions: Record; - } -) => { - // Prevents focus from moving to the button. - const addFileButtonMouseDownHandler = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - }, - [] - ); - // Opens the file toolbar. - const addFileButtonClickHandler = useCallback(() => { - props.editor._tiptapEditor.view.dispatch( - props.editor._tiptapEditor.state.tr.setMeta( - props.editor.filePanel!.plugin, - { - block: props.block, - } - ) - ); - }, [ - props.block, - props.editor._tiptapEditor.state.tr, - props.editor._tiptapEditor.view, - props.editor.filePanel, - ]); - - const activeExtension: ReactFileBlockExtension | undefined = useMemo(() => { - if ( - props.block.props.fileType && - props.extensions && - props.block.props.fileType in props.extensions - ) { - return props.extensions[props.block.props.fileType]; - } - - return undefined; - }, [props.block.props.fileType, props.extensions]); - - const FileComponent: FC< - Omit< - ReactCustomBlockRenderProps, - "contentRef" - > - > = useMemo( - () => - props.block.props.showPreview - ? activeExtension?.render || DefaultFileRender - : DefaultFileRender, - [activeExtension?.render, props.block.props.showPreview] - ); - - return ( -
- {props.block.props.url === "" ? ( -
-
- {activeExtension?.buttonIcon || } -
-
{`${ - props.editor.dictionary.file.button_add_text - } ${ - activeExtension?.buttonText || - props.editor.dictionary.file.button_default_file_type_text - }`}
-
- ) : ( -
- - {props.block.props.caption && ( -

- {props.block.props.caption} -

- )} -
- )} -
- ); -}; - export const FileToExternalHTML = (props: { block: BlockFromConfig; editor: BlockNoteEditor< @@ -141,10 +40,6 @@ export const FileToExternalHTML = (props: { any, any >; - extensions?: Record< - string, - Pick - >; }) => { if (!props.block.props.url) { const buttonText = `${props.editor.dictionary.file.button_add_text} ${ @@ -190,27 +85,18 @@ export const FileToExternalHTML = (props: { return embed; }; -export const createReactFileBlockImplementation = ( - extensions: Record< - string, - ReactFileBlockExtension - > = defaultReactFileExtensions -) => - ({ - render: (props) => , - parse: (element) => fileParse(element, extensions), - toExternalHTML: (props) => ( - - ), - } satisfies ReactCustomBlockImplementation); - -export const createReactFileBlock = ( - extensions: Record< - string, - ReactFileBlockExtension - > = defaultReactFileExtensions -) => - createReactBlockSpec( - fileBlockConfig, - createReactFileBlockImplementation(extensions) - ); +export const ReactFileBlock = createReactBlockSpec(fileBlockConfig, { + render: (props) => ( +
+ {props.block.props.url === "" ? ( + + ) : ( + + + + )} +
+ ), + parse: (element) => fileParse(element), + toExternalHTML: (props) => , +}); diff --git a/packages/react/src/components/FileBlock/ImageBlockContent.tsx b/packages/react/src/components/FileBlock/ImageBlockContent.tsx new file mode 100644 index 0000000000..3339e5e43b --- /dev/null +++ b/packages/react/src/components/FileBlock/ImageBlockContent.tsx @@ -0,0 +1,69 @@ +import { + BlockFromConfig, + BlockNoteEditor, + BlockSchemaWithBlock, + fileParse, + imageBlockConfig, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; + +import { useState } from "react"; +import { createReactBlockSpec } from "../../schema/ReactBlockSpec"; +import { ResizeHandlesWrapper } from "./extensions/utils/ResizeHandlesWrapper"; +import { FileAndCaption, FileAndPlaceholder } from "./fileBlockHelpers"; + +const ImageRender = < + ISchema extends InlineContentSchema, + SSchema extends StyleSchema +>(props: { + block: BlockFromConfig; + editor: BlockNoteEditor< + BlockSchemaWithBlock<"image", typeof imageBlockConfig>, + ISchema, + SSchema + >; +}) => { + const [width, setWidth] = useState( + Math.min( + props.block.props.previewWidth, + props.editor.domElement.firstElementChild!.clientWidth + ) + ); + + return ( + + {props.block.props.caption + + ); +}; + +export const ReactImageBlock = createReactBlockSpec(imageBlockConfig, { + render: (props) => ( +
+ {props.block.props.url === "" ? ( + // // TODO: pass image related things + + ) : !props.block.props.showPreview ? ( + +
TODO
+ {/* TODO */} + {/* */} +
+ ) : ( + + + + )} +
+ ), + parse: (element) => fileParse(element), + // toExternalHTML: (props) => , +}); diff --git a/packages/react/src/components/FileBlock/extensions/defaultReactFileExtensions.ts b/packages/react/src/components/FileBlock/extensions/defaultReactFileExtensions.ts deleted file mode 100644 index e28ec19b61..0000000000 --- a/packages/react/src/components/FileBlock/extensions/defaultReactFileExtensions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ReactFileBlockExtension } from "../reactFileBlockExtension"; -import { reactImageFileExtension } from "./reactImageFileExtension"; -import { reactVideoFileExtension } from "./reactVideoFileExtension"; -import { reactAudioFileExtension } from "./reactAudioFileExtension"; - -export const defaultReactFileExtensions: Record< - string, - ReactFileBlockExtension -> = { - image: reactImageFileExtension, - video: reactVideoFileExtension, - audio: reactAudioFileExtension, -}; diff --git a/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx b/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx index 81180c2f61..52eb239d15 100644 --- a/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx +++ b/packages/react/src/components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx @@ -1,8 +1,7 @@ import { BlockFromConfig, BlockNoteEditor, - BlockSchemaWithBlock, - DefaultBlockSchema, + FileBlockConfig, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -12,9 +11,9 @@ export const ResizeHandlesWrapper = < ISchema extends InlineContentSchema, SSchema extends StyleSchema >(props: { - block: BlockFromConfig; + block: BlockFromConfig; editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", DefaultBlockSchema["file"]>, + any, // TODO: BlockSchemaWithBlock<"file", DefaultBlockSchema["file"]> ? ISchema, SSchema >; diff --git a/packages/react/src/components/FileBlock/fileBlockHelpers.tsx b/packages/react/src/components/FileBlock/fileBlockHelpers.tsx new file mode 100644 index 0000000000..a12644ecfb --- /dev/null +++ b/packages/react/src/components/FileBlock/fileBlockHelpers.tsx @@ -0,0 +1,76 @@ +import { FileBlockConfig } from "@blocknote/core"; +import { useCallback } from "react"; +import { RiFile2Line } from "react-icons/ri"; +import { ReactCustomBlockRenderProps } from "../../schema/ReactBlockSpec"; + +export const FileAndCaption = ( + props: Omit< + ReactCustomBlockRenderProps, + "contentRef" + > & { + children: React.ReactNode; + } +) => { + return ( +
+ {props.children} + {props.block.props.caption && ( +

+ {props.block.props.caption} +

+ )} +
+ ); +}; + +export const FileAndPlaceholder = ( + props: Omit< + ReactCustomBlockRenderProps, + "contentRef" + > +) => { + // Prevents focus from moving to the button. + const addFileButtonMouseDownHandler = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + }, + [] + ); + // Opens the file toolbar. + const addFileButtonClickHandler = useCallback(() => { + props.editor._tiptapEditor.view.dispatch( + props.editor._tiptapEditor.state.tr.setMeta( + props.editor.filePanel!.plugin, + { + block: props.block, + } + ) + ); + }, [ + props.block, + props.editor._tiptapEditor.state.tr, + props.editor._tiptapEditor.view, + props.editor.filePanel, + ]); + + return ( +
+
+ {/* TODO: customizable */} + + {/* {activeExtension?.buttonIcon || } */} +
+
+ {/* TODO: customizable */} + {props.editor.dictionary.file.button_add_file_text} +
+
+ ); +}; diff --git a/packages/react/src/components/FileBlock/reactFileBlockExtension.tsx b/packages/react/src/components/FileBlock/reactFileBlockExtension.tsx deleted file mode 100644 index 9b7eb00cf6..0000000000 --- a/packages/react/src/components/FileBlock/reactFileBlockExtension.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { fileBlockConfig, PartialBlockFromConfig } from "@blocknote/core"; -import { FC } from "react"; -import { ReactCustomBlockRenderProps } from "../../schema/ReactBlockSpec"; - -export type ReactFileBlockExtension = { - fileEndings: string[]; - render: FC< - Omit< - ReactCustomBlockRenderProps, - "contentRef" - > - >; - toExternalHTML?: FC< - Omit< - ReactCustomBlockRenderProps, - "contentRef" - > - >; - parse?: ( - element: HTMLElement - ) => - | PartialBlockFromConfig["props"] - | undefined; - buttonText?: string; - buttonIcon?: JSX.Element; -}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9c93d7c2d5..e78c26349e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,9 +6,7 @@ export * from "./editor/ComponentsContext"; export * from "./i18n/dictionary"; export * from "./components/FileBlock/FileBlockContent"; -export * from "./components/FileBlock/reactFileBlockExtension"; -export * from "./components/FileBlock/extensions/reactImageFileExtension"; -export * from "./components/FileBlock/extensions/defaultReactFileExtensions"; +export * from "./components/FileBlock/ImageBlockContent"; export * from "./components/FormattingToolbar/DefaultButtons/BasicTextStyleButton"; export * from "./components/FormattingToolbar/DefaultButtons/ColorStyleButton"; From ed9c3dff2820713dbcfb33d63e8a76576a6a8201 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 17 May 2024 21:01:40 +0200 Subject: [PATCH 6/7] Finished core refactor --- .../AudioBlockContent/AudioBlockContent.ts | 148 ++++++++++ .../AudioBlockContent/audioBlockHelpers.ts | 5 + .../FileBlockContent/FileBlockContent.ts | 62 ++-- .../extensions/audioFileExtension.ts | 78 ----- .../extensions/defaultFileExtensions.ts | 10 - .../extensions/imageFileExtension.ts | 109 ------- .../utils/renderWithResizeHandles.ts | 209 -------------- .../extensions/videoFileExtension.ts | 96 ------- .../FileBlockContent/fileBlockHelpers.ts | 269 +++++++++++++++--- .../ImageBlockContent.ts | 71 +++-- .../ImageBlockContent/imageBlockHelpers.ts | 6 + .../VideoBlockContent/VideoBlockContent.ts | 167 +++++++++++ .../VideoBlockContent/videoBlockHelpers.ts | 6 + packages/core/src/blocks/defaultBlocks.ts | 6 +- packages/core/src/editor/Block.css | 31 +- .../getDefaultSlashMenuItems.ts | 38 +++ packages/core/src/index.ts | 5 +- packages/core/src/schema/blocks/createSpec.ts | 1 + packages/core/src/schema/blocks/internal.ts | 5 + 19 files changed, 688 insertions(+), 634 deletions(-) create mode 100644 packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts create mode 100644 packages/core/src/blocks/AudioBlockContent/audioBlockHelpers.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/extensions/audioFileExtension.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/extensions/defaultFileExtensions.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/extensions/imageFileExtension.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts delete mode 100644 packages/core/src/blocks/FileBlockContent/extensions/videoFileExtension.ts rename packages/core/src/blocks/{FileBlockContent => ImageBlockContent}/ImageBlockContent.ts (62%) create mode 100644 packages/core/src/blocks/ImageBlockContent/imageBlockHelpers.ts create mode 100644 packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts create mode 100644 packages/core/src/blocks/VideoBlockContent/videoBlockHelpers.ts diff --git a/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts b/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts new file mode 100644 index 0000000000..e3d2555f40 --- /dev/null +++ b/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts @@ -0,0 +1,148 @@ +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { + BlockFromConfig, + createBlockSpec, + FileBlockConfig, + Props, + PropSchema, +} from "../../schema"; +import { defaultProps } from "../defaultProps"; + +import { + createAddFileButton, + createDefaultFilePreview, + createFigureWithCaption, + createFileAndCaptionWrapper, + parseFigureElement, +} from "../FileBlockContent/fileBlockHelpers"; +import { parseAudioElement } from "./audioBlockHelpers"; + +export const audioPropSchema = { + backgroundColor: defaultProps.backgroundColor, + // File name. + name: { + default: "" as const, + }, + // File url. + url: { + default: "" as const, + }, + // File caption. + caption: { + default: "" as const, + }, + + showPreview: { + default: true, + }, +} satisfies PropSchema; + +export const audioBlockConfig = { + type: "audio" as const, + propSchema: audioPropSchema, + content: "none", + isFileBlock: true, +} satisfies FileBlockConfig; + +export const audioRender = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + const wrapper = document.createElement("div"); + wrapper.className = "bn-file-block-content-wrapper"; + + if (block.props.url === "") { + const fileBlockAudioIcon = document.createElement("div"); + fileBlockAudioIcon.innerHTML = + ''; + const addAudioButton = createAddFileButton( + block, + editor, + "Add audio", + fileBlockAudioIcon.firstElementChild as HTMLElement + ); + wrapper.appendChild(addAudioButton.dom); + + return { + dom: wrapper, + destroy: () => { + addAudioButton?.destroy?.(); + }, + }; + } else if (!block.props.showPreview) { + const file = createDefaultFilePreview(block).dom; + const element = createFileAndCaptionWrapper(block, file); + + return { + dom: element.dom, + }; + } else { + const audio = document.createElement("audio"); + audio.className = "bn-audio"; + audio.src = block.props.url; + audio.controls = true; + audio.contentEditable = "false"; + audio.draggable = false; + + const element = createFileAndCaptionWrapper(block, audio); + wrapper.appendChild(element.dom); + + return { + dom: wrapper, + }; + } +}; + +export const audioParse = ( + element: HTMLElement +): Partial> | undefined => { + if (element.tagName === "AUDIO") { + return parseAudioElement(element as HTMLAudioElement); + } + + if (element.tagName === "FIGURE") { + const parsedFigure = parseFigureElement(element, "audio"); + if (!parsedFigure) { + return undefined; + } + + const { targetElement, caption } = parsedFigure; + + return { + ...parseAudioElement(targetElement as HTMLAudioElement), + caption, + }; + } + + return undefined; +}; + +export const audioToExternalHTML = ( + block: BlockFromConfig +) => { + if (!block.props.url) { + const div = document.createElement("p"); + div.innerHTML = "Add audio"; + + return { + dom: div, + }; + } + + const audio = document.createElement("audio"); + audio.src = block.props.url; + + if (block.props.caption) { + return createFigureWithCaption(audio, block.props.caption); + } + + return { + dom: audio, + }; +}; + +export const AudioBlock = createBlockSpec(audioBlockConfig, { + render: audioRender, + parse: audioParse, + toExternalHTML: audioToExternalHTML, +}); diff --git a/packages/core/src/blocks/AudioBlockContent/audioBlockHelpers.ts b/packages/core/src/blocks/AudioBlockContent/audioBlockHelpers.ts new file mode 100644 index 0000000000..2a5bec4403 --- /dev/null +++ b/packages/core/src/blocks/AudioBlockContent/audioBlockHelpers.ts @@ -0,0 +1,5 @@ +export const parseAudioElement = (audioElement: HTMLAudioElement) => { + const url = audioElement.src || undefined; + + return { url }; +}; diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index 5d7c4f88f0..f352228adb 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -6,14 +6,12 @@ import { createBlockSpec, } from "../../schema"; import { defaultProps } from "../defaultProps"; - import { - createFileAndCaptionDOM, - createFileIconAndNameDOM, - createFilePlaceholderDOM, - parseEmbed, - parseFigure, - toExternalHTMLWithCaption, + createFileAndCaptionWrapper, + createDefaultFilePreview, + createAddFileButton, + parseEmbedElement, + parseFigureElement, } from "./fileBlockHelpers"; export const filePropSchema = { @@ -49,26 +47,17 @@ export const fileRender = ( const wrapper = document.createElement("div"); wrapper.className = "bn-file-block-content-wrapper"; - // const fileType = block.props.fileType; - // block.props.showPreview && fileType && extensions && fileType in extensions - // ? extensions[fileType].render(block, editor) - // : defaultFileRender(block); - - // File element. - if (block.props.url === "") { - const placeholder = createFilePlaceholderDOM(block, editor); - wrapper.appendChild(placeholder.dom); + const addFileButton = createAddFileButton(block, editor); + wrapper.appendChild(addFileButton.dom); return { dom: wrapper, - destroy: () => { - placeholder?.destroy?.(); - }, + destroy: addFileButton.destroy, }; } else { - const file = createFileIconAndNameDOM(block).dom; - const element = createFileAndCaptionDOM(block, editor, file); + const file = createDefaultFilePreview(block).dom; + const element = createFileAndCaptionWrapper(block, file); wrapper.appendChild(element.dom); return { @@ -79,11 +68,11 @@ export const fileRender = ( export const fileParse = (element: HTMLElement) => { if (element.tagName === "EMBED") { - return parseEmbed(element as HTMLEmbedElement); + return parseEmbedElement(element as HTMLEmbedElement); } if (element.tagName === "FIGURE") { - const parsedFigure = parseFigure(element, "embed"); + const parsedFigure = parseFigureElement(element, "embed"); if (!parsedFigure) { return undefined; } @@ -91,7 +80,7 @@ export const fileParse = (element: HTMLElement) => { const { targetElement, caption } = parsedFigure; return { - ...parseEmbed(targetElement as HTMLEmbedElement), + ...parseEmbedElement(targetElement as HTMLEmbedElement), caption, }; } @@ -111,16 +100,20 @@ export const fileToExternalHTML = ( }; } - // TBD: should default be of type "embed"? - const embed = document.createElement("embed"); - embed.src = block.props.url; + const wrapper = document.createElement("div"); + const fileSrcLink = document.createElement("a"); + fileSrcLink.href = block.props.url; + fileSrcLink.innerText = block.props.name; + wrapper.appendChild(fileSrcLink); if (block.props.caption) { - return toExternalHTMLWithCaption(embed, block.props.caption); + const fileCaption = document.createElement("p"); + fileCaption.innerText = block.props.caption; + wrapper.appendChild(fileCaption); } return { - dom: embed, + dom: wrapper, }; }; @@ -129,14 +122,3 @@ export const FileBlock = createBlockSpec(fileBlockConfig, { parse: fileParse, toExternalHTML: fileToExternalHTML, }); - -// - React support? -// - Support parse HTML and toExternalHTML -// - Copy/paste support -// - Drag/drop from external into BlockNote (automatic conversion to block & upload) -// - Custom props e.g. PDF height, video playback options -// - Toolbar/menu options should change based on file type -// - Button text/icon should be file type specific -// - Should be able to define the file type before uploading/embedding -// - Media like pdfs should be previewable (might be issues with cross domain access) -// - Renderers should be loosely coupled to file extensions via plugins for different file types diff --git a/packages/core/src/blocks/FileBlockContent/extensions/audioFileExtension.ts b/packages/core/src/blocks/FileBlockContent/extensions/audioFileExtension.ts deleted file mode 100644 index d83c6cb380..0000000000 --- a/packages/core/src/blocks/FileBlockContent/extensions/audioFileExtension.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { BlockFromConfig } from "../../../schema"; -import { fileBlockConfig } from "../fileBlockConfig"; -import { FileBlockExtension } from "../fileBlockExtension"; - -export const audioRender = ( - block: BlockFromConfig -) => { - // Audio element. - const audio = document.createElement("audio"); - audio.className = "bn-audio"; - audio.src = block.props.url; - audio.contentEditable = "false"; - audio.controls = true; - audio.draggable = false; - - return { - dom: audio, - }; -}; - -export const audioParse = (element: HTMLElement) => { - if (element.tagName === "FIGURE") { - const audio = element.querySelector("audio"); - const caption = element.querySelector("figcaption"); - return { - fileType: "audio", - url: audio?.src || undefined, - caption: caption?.textContent ?? undefined, - }; - } - - if (element.tagName === "AUDIO") { - return { - fileType: "audio", - url: (element as HTMLAudioElement).src || undefined, - }; - } - - return undefined; -}; - -export const audioToExternalHTML = ( - block: BlockFromConfig -) => { - const audio = document.createElement("audio"); - audio.src = block.props.url; - - if (block.props.caption) { - const figure = document.createElement("figure"); - const caption = document.createElement("figcaption"); - caption.textContent = block.props.caption; - - figure.appendChild(audio); - figure.appendChild(caption); - - return { - dom: figure, - }; - } - - return { - dom: audio, - }; -}; - -export const audioFileExtension: FileBlockExtension = { - fileEndings: ["flac", "mp3", "wav"], - render: audioRender, - parse: audioParse, - toExternalHTML: audioToExternalHTML, - buttonText: "audio", - buttonIcon: () => { - const fileBlockAudioIcon = document.createElement("div"); - fileBlockAudioIcon.innerHTML = - ''; - return fileBlockAudioIcon.firstElementChild as HTMLElement; - }, -}; diff --git a/packages/core/src/blocks/FileBlockContent/extensions/defaultFileExtensions.ts b/packages/core/src/blocks/FileBlockContent/extensions/defaultFileExtensions.ts deleted file mode 100644 index c6f5a5ff7f..0000000000 --- a/packages/core/src/blocks/FileBlockContent/extensions/defaultFileExtensions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FileBlockExtension } from "../fileBlockExtension"; -import { imageFileExtension } from "./imageFileExtension"; -import { videoFileExtension } from "./videoFileExtension"; -import { audioFileExtension } from "./audioFileExtension"; - -export const defaultFileExtensions: Record = { - image: imageFileExtension, - video: videoFileExtension, - audio: audioFileExtension, -}; diff --git a/packages/core/src/blocks/FileBlockContent/extensions/imageFileExtension.ts b/packages/core/src/blocks/FileBlockContent/extensions/imageFileExtension.ts deleted file mode 100644 index 7ae9b15b96..0000000000 --- a/packages/core/src/blocks/FileBlockContent/extensions/imageFileExtension.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; -import { BlockFromConfig, BlockSchemaWithBlock } from "../../../schema"; -import { fileBlockConfig } from "../fileBlockConfig"; -import { FileBlockExtension } from "../fileBlockExtension"; -import { renderWithResizeHandles } from "./utils/renderWithResizeHandles"; - -export const imageRender = ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - > -) => { - // Image element. - const image = document.createElement("img"); - image.className = "bn-visual-media"; - image.src = block.props.url; - image.alt = block.props.caption || "BlockNote image"; - image.contentEditable = "false"; - image.draggable = false; - image.width = Math.min( - block.props.previewWidth, - editor.domElement.firstElementChild!.clientWidth - ); - - return renderWithResizeHandles( - block, - editor, - image, - () => image.width, - (width) => (image.width = width) - ); -}; - -export const imageParse = (element: HTMLElement) => { - if (element.tagName === "FIGURE") { - const img = element.querySelector("img"); - const caption = element.querySelector("figcaption"); - return { - fileType: "image", - url: img?.src || undefined, - caption: caption?.textContent ?? img?.alt, - previewWidth: img?.width || undefined, - }; - } - - if (element.tagName === "IMG") { - return { - fileType: "image", - url: (element as HTMLImageElement).src || undefined, - previewWidth: (element as HTMLImageElement).width || undefined, - }; - } - - return undefined; -}; - -export const imageToExternalHTML = ( - block: BlockFromConfig -) => { - const image = document.createElement("img"); - image.src = block.props.url; - image.width = block.props.previewWidth; - image.alt = block.props.caption || "BlockNote image"; - - if (block.props.caption) { - const figure = document.createElement("figure"); - const caption = document.createElement("figcaption"); - caption.textContent = block.props.caption; - - figure.appendChild(image); - figure.appendChild(caption); - - return { - dom: figure, - }; - } - - return { - dom: image, - }; -}; - -export const imageFileExtension: FileBlockExtension = { - fileEndings: [ - "apng", - "avif", - "gif", - "jpg", - "jpeg", - "jfif", - "pjpeg", - "pjp", - "svg", - "webp", - ], - render: imageRender, - parse: imageParse, - toExternalHTML: imageToExternalHTML, - buttonText: "image", - buttonIcon: () => { - const fileBlockImageIcon = document.createElement("div"); - fileBlockImageIcon.innerHTML = - ''; - - return fileBlockImageIcon.firstElementChild as HTMLElement; - }, -}; diff --git a/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts b/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts deleted file mode 100644 index 33712e8629..0000000000 --- a/packages/core/src/blocks/FileBlockContent/extensions/utils/renderWithResizeHandles.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; -import { BlockFromConfig, BlockSchemaWithBlock } from "../../../../schema"; -import { fileBlockConfig } from "../../fileBlockConfig"; - -export const renderWithResizeHandles = ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - >, - element: HTMLElement, - getWidth: () => number, - setWidth: (width: number) => void -): { dom: HTMLElement; destroy: () => void } => { - // Wrapper element for rendered element and resize handles. - const wrapper = document.createElement("div"); - wrapper.className = "bn-visual-media-wrapper"; - - // Resize handle elements. - const leftResizeHandle = document.createElement("div"); - leftResizeHandle.className = "bn-visual-media-resize-handle"; - leftResizeHandle.style.left = "4px"; - const rightResizeHandle = document.createElement("div"); - rightResizeHandle.className = "bn-visual-media-resize-handle"; - rightResizeHandle.style.right = "4px"; - - // Temporary parameters set when the user begins resizing the element, used to - // calculate the new width of the element. - let resizeParams: - | { - handleUsed: "left" | "right"; - initialWidth: number; - initialClientX: number; - } - | undefined; - - // Updates the element width with an updated width depending on the cursor X - // offset from when the resize began, and which resize handle is being used. - const windowMouseMoveHandler = (event: MouseEvent) => { - if (!resizeParams) { - if ( - !editor.isEditable && - wrapper.contains(leftResizeHandle) && - wrapper.contains(rightResizeHandle) - ) { - wrapper.removeChild(leftResizeHandle); - wrapper.removeChild(rightResizeHandle); - } - - return; - } - - let newWidth: number; - - if (block.props.textAlignment === "center") { - if (resizeParams.handleUsed === "left") { - newWidth = - resizeParams.initialWidth + - (resizeParams.initialClientX - event.clientX) * 2; - } else { - newWidth = - resizeParams.initialWidth + - (event.clientX - resizeParams.initialClientX) * 2; - } - } else { - if (resizeParams.handleUsed === "left") { - newWidth = - resizeParams.initialWidth + - resizeParams.initialClientX - - event.clientX; - } else { - newWidth = - resizeParams.initialWidth + - event.clientX - - resizeParams.initialClientX; - } - } - - // Min element width in px. - const minWidth = 64; - - // Ensures the element is not wider than the editor and not smaller than a - // predetermined minimum width. - if (newWidth < minWidth) { - setWidth(minWidth); - } else if (newWidth > editor.domElement.firstElementChild!.clientWidth) { - setWidth(editor.domElement.firstElementChild!.clientWidth); - } else { - setWidth(newWidth); - } - }; - // Stops mouse movements from resizing the element and updates the block's - // `width` prop to the new value. - const windowMouseUpHandler = (event: MouseEvent) => { - // Hides the drag handles if the cursor is no longer over the element. - if ( - (!event.target || - !wrapper.contains(event.target as Node) || - !editor.isEditable) && - wrapper.contains(leftResizeHandle) && - wrapper.contains(rightResizeHandle) - ) { - wrapper.removeChild(leftResizeHandle); - wrapper.removeChild(rightResizeHandle); - } - - if (!resizeParams) { - return; - } - - resizeParams = undefined; - - editor.updateBlock(block, { - props: { - previewWidth: getWidth(), - }, - }); - }; - - // Shows the resize handles when hovering over the element with the cursor. - const elementMouseEnterHandler = () => { - if (editor.isEditable) { - wrapper.appendChild(leftResizeHandle); - wrapper.appendChild(rightResizeHandle); - } - }; - // Hides the resize handles when the cursor leaves the element, unless the - // cursor moves to one of the resize handles. - const elementMouseLeaveHandler = (event: MouseEvent) => { - if ( - event.relatedTarget === leftResizeHandle || - event.relatedTarget === rightResizeHandle - ) { - return; - } - - if (resizeParams) { - return; - } - - if ( - editor.isEditable && - wrapper.contains(leftResizeHandle) && - wrapper.contains(rightResizeHandle) - ) { - wrapper.removeChild(leftResizeHandle); - wrapper.removeChild(rightResizeHandle); - } - }; - - // Sets the resize params, allowing the user to begin resizing the element by - // moving the cursor left or right. - const leftResizeHandleMouseDownHandler = (event: MouseEvent) => { - event.preventDefault(); - - wrapper.appendChild(leftResizeHandle); - wrapper.appendChild(rightResizeHandle); - - resizeParams = { - handleUsed: "left", - initialWidth: block.props.previewWidth, - initialClientX: event.clientX, - }; - }; - const rightResizeHandleMouseDownHandler = (event: MouseEvent) => { - event.preventDefault(); - - wrapper.appendChild(leftResizeHandle); - wrapper.appendChild(rightResizeHandle); - - resizeParams = { - handleUsed: "right", - initialWidth: block.props.previewWidth, - initialClientX: event.clientX, - }; - }; - - wrapper.appendChild(element); - - window.addEventListener("mousemove", windowMouseMoveHandler); - window.addEventListener("mouseup", windowMouseUpHandler); - element.addEventListener("mouseenter", elementMouseEnterHandler); - element.addEventListener("mouseleave", elementMouseLeaveHandler); - leftResizeHandle.addEventListener( - "mousedown", - leftResizeHandleMouseDownHandler - ); - rightResizeHandle.addEventListener( - "mousedown", - rightResizeHandleMouseDownHandler - ); - - return { - dom: wrapper, - destroy: () => { - window.removeEventListener("mousemove", windowMouseMoveHandler); - window.removeEventListener("mouseup", windowMouseUpHandler); - leftResizeHandle.removeEventListener( - "mousedown", - leftResizeHandleMouseDownHandler - ); - rightResizeHandle.removeEventListener( - "mousedown", - rightResizeHandleMouseDownHandler - ); - }, - }; -}; diff --git a/packages/core/src/blocks/FileBlockContent/extensions/videoFileExtension.ts b/packages/core/src/blocks/FileBlockContent/extensions/videoFileExtension.ts deleted file mode 100644 index f4c9eceee0..0000000000 --- a/packages/core/src/blocks/FileBlockContent/extensions/videoFileExtension.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; -import { BlockFromConfig, BlockSchemaWithBlock } from "../../../schema"; -import { fileBlockConfig } from "../fileBlockConfig"; -import { FileBlockExtension } from "../fileBlockExtension"; -import { renderWithResizeHandles } from "./utils/renderWithResizeHandles"; - -export const videoRender = ( - block: BlockFromConfig, - editor: BlockNoteEditor< - BlockSchemaWithBlock<"file", typeof fileBlockConfig>, - any, - any - > -) => { - // Video element. - const video = document.createElement("video"); - video.className = "bn-visual-media"; - video.src = block.props.url; - video.controls = true; - video.contentEditable = "false"; - video.draggable = false; - video.width = Math.min( - block.props.previewWidth, - editor.domElement.firstElementChild!.clientWidth - ); - - return renderWithResizeHandles( - block, - editor, - video, - () => video.width, - (width) => (video.width = width) - ); -}; - -export const videoParse = (element: HTMLElement) => { - if (element.tagName === "FIGURE") { - const img = element.querySelector("video"); - const caption = element.querySelector("figcaption"); - return { - fileType: "video", - url: img?.src || undefined, - caption: caption?.textContent ?? undefined, - previewWidth: img?.width || undefined, - }; - } - - if (element.tagName === "VIDEO") { - return { - fileType: "video", - url: (element as HTMLVideoElement).src || undefined, - previewWidth: (element as HTMLVideoElement).width || undefined, - }; - } - - return undefined; -}; - -export const videoToExternalHTML = ( - block: BlockFromConfig -) => { - const video = document.createElement("video"); - video.src = block.props.url; - video.width = block.props.previewWidth; - - if (block.props.caption) { - const figure = document.createElement("figure"); - const caption = document.createElement("figcaption"); - caption.textContent = block.props.caption; - - figure.appendChild(video); - figure.appendChild(caption); - - return { - dom: figure, - }; - } - - return { - dom: video, - }; -}; - -export const videoFileExtension: FileBlockExtension = { - fileEndings: ["mp4", "ogg", "webm"], - render: videoRender, - parse: videoParse, - toExternalHTML: videoToExternalHTML, - buttonText: "video", - buttonIcon: () => { - const fileBlockVideoIcon = document.createElement("div"); - fileBlockVideoIcon.innerHTML = - ''; - return fileBlockVideoIcon.firstElementChild as HTMLElement; - }, -}; diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts index 9f659275d4..7a2391b708 100644 --- a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -1,7 +1,8 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockFromConfig, FileBlockConfig } from "../../schema"; -export const createFileIconAndNameDOM = ( +// Default file preview, displaying a file icon and file name. +export const createDefaultFilePreview = ( block: BlockFromConfig ): { dom: HTMLElement; destroy?: () => void } => { const file = document.createElement("div"); @@ -24,16 +25,14 @@ export const createFileIconAndNameDOM = ( }; }; -export const createFileAndCaptionDOM = ( +// Wrapper element containing file preview and caption. +export const createFileAndCaptionWrapper = ( block: BlockFromConfig, - editor: BlockNoteEditor, file: HTMLElement ) => { - // Wrapper element for the file, resize handles and caption. const fileAndCaptionWrapper = document.createElement("div"); fileAndCaptionWrapper.className = "bn-file-and-caption-wrapper"; - // Caption element. const caption = document.createElement("p"); caption.className = "bn-file-caption"; caption.innerText = block.props.caption; @@ -46,32 +45,28 @@ export const createFileAndCaptionDOM = ( }; }; -// TODO: allow icon / text to be passed in -export const createFilePlaceholderDOM = ( +// Button element that acts as a placeholder for files with no src. +export const createAddFileButton = ( block: BlockFromConfig, - editor: BlockNoteEditor + editor: BlockNoteEditor, + buttonText?: string, + buttonIcon?: HTMLElement ) => { - // Button element that acts as a placeholder for files with no src. const addFileButton = document.createElement("div"); addFileButton.className = "bn-add-file-button"; - // Icon for the add file button. const addFileButtonIcon = document.createElement("div"); addFileButtonIcon.className = "bn-add-file-button-icon"; + if (buttonIcon) { + addFileButtonIcon.appendChild(buttonIcon); + } else { + addFileButtonIcon.innerHTML = + ''; + } - // Text for the add file button. const addFileButtonText = document.createElement("p"); addFileButtonText.className = "bn-add-file-button-text"; - - addFileButtonText.innerHTML = - /*`${editor.dictionary.file.button_add_text} ${ - block.props.fileType && - extensions && - block.props.fileType in extensions && - extensions[block.props.fileType].buttonText !== undefined - ? extensions[block.props.fileType].buttonText! - : */ editor.dictionary.file.button_add_file_text; - // }`; + addFileButtonText.innerHTML = buttonText || "Add file"; // Prevents focus from moving to the button. const addFileButtonMouseDownHandler = (event: MouseEvent) => { @@ -86,19 +81,6 @@ export const createFilePlaceholderDOM = ( ); }; - // if ( - // block.props.fileType && - // extensions && - // block.props.fileType in extensions && - // extensions[block.props.fileType].buttonIcon !== undefined - // ) { - // addFileButtonIcon.appendChild( - // extensions[block.props.fileType].buttonIcon!() - // ); - // } else { - addFileButtonIcon.innerHTML = - ''; - // } addFileButton.appendChild(addFileButtonIcon); addFileButton.appendChild(addFileButtonText); @@ -126,13 +108,16 @@ export const createFilePlaceholderDOM = ( }; }; -export const parseEmbed = (embedElement: HTMLEmbedElement) => { +export const parseEmbedElement = (embedElement: HTMLEmbedElement) => { const url = embedElement.src || undefined; return { url }; }; -export const parseFigure = (figureElement: HTMLElement, targetTag: string) => { +export const parseFigureElement = ( + figureElement: HTMLElement, + targetTag: string +) => { const targetElement = figureElement.querySelector( targetTag ) as HTMLElement | null; @@ -146,7 +131,9 @@ export const parseFigure = (figureElement: HTMLElement, targetTag: string) => { return { targetElement, caption }; }; -export const toExternalHTMLWithCaption = ( +// Wrapper figure element to display file preview with caption. Used for +// external HTML. +export const createFigureWithCaption = ( element: HTMLElement, caption: string ) => { @@ -159,3 +146,211 @@ export const toExternalHTMLWithCaption = ( return { dom: figure }; }; + +// Wrapper element which adds resize handles & logic for visual media file +// previews. +export const createResizeHandlesWrapper = ( + block: BlockFromConfig, + editor: BlockNoteEditor, + element: HTMLElement, + getWidth: () => number, + setWidth: (width: number) => void +): { dom: HTMLElement; destroy: () => void } => { + if (!block.props.previewWidth) { + throw new Error("Block must have a `previewWidth` prop."); + } + + // Wrapper element for rendered element and resize handles. + const wrapper = document.createElement("div"); + wrapper.className = "bn-visual-media-wrapper"; + + // Resize handle elements. + const leftResizeHandle = document.createElement("div"); + leftResizeHandle.className = "bn-visual-media-resize-handle"; + leftResizeHandle.style.left = "4px"; + const rightResizeHandle = document.createElement("div"); + rightResizeHandle.className = "bn-visual-media-resize-handle"; + rightResizeHandle.style.right = "4px"; + + // Temporary parameters set when the user begins resizing the element, used to + // calculate the new width of the element. + let resizeParams: + | { + handleUsed: "left" | "right"; + initialWidth: number; + initialClientX: number; + } + | undefined; + + // Updates the element width with an updated width depending on the cursor X + // offset from when the resize began, and which resize handle is being used. + const windowMouseMoveHandler = (event: MouseEvent) => { + if (!resizeParams) { + if ( + !editor.isEditable && + wrapper.contains(leftResizeHandle) && + wrapper.contains(rightResizeHandle) + ) { + wrapper.removeChild(leftResizeHandle); + wrapper.removeChild(rightResizeHandle); + } + + return; + } + + let newWidth: number; + + if (block.props.textAlignment === "center") { + if (resizeParams.handleUsed === "left") { + newWidth = + resizeParams.initialWidth + + (resizeParams.initialClientX - event.clientX) * 2; + } else { + newWidth = + resizeParams.initialWidth + + (event.clientX - resizeParams.initialClientX) * 2; + } + } else { + if (resizeParams.handleUsed === "left") { + newWidth = + resizeParams.initialWidth + + resizeParams.initialClientX - + event.clientX; + } else { + newWidth = + resizeParams.initialWidth + + event.clientX - + resizeParams.initialClientX; + } + } + + // Min element width in px. + const minWidth = 64; + + // Ensures the element is not wider than the editor and not smaller than a + // predetermined minimum width. + if (newWidth < minWidth) { + setWidth(minWidth); + } else if (newWidth > editor.domElement.firstElementChild!.clientWidth) { + setWidth(editor.domElement.firstElementChild!.clientWidth); + } else { + setWidth(newWidth); + } + }; + // Stops mouse movements from resizing the element and updates the block's + // `width` prop to the new value. + const windowMouseUpHandler = (event: MouseEvent) => { + // Hides the drag handles if the cursor is no longer over the element. + if ( + (!event.target || + !wrapper.contains(event.target as Node) || + !editor.isEditable) && + wrapper.contains(leftResizeHandle) && + wrapper.contains(rightResizeHandle) + ) { + wrapper.removeChild(leftResizeHandle); + wrapper.removeChild(rightResizeHandle); + } + + if (!resizeParams) { + return; + } + + resizeParams = undefined; + + editor.updateBlock(block, { + props: { + previewWidth: getWidth(), + }, + }); + }; + + // Shows the resize handles when hovering over the element with the cursor. + const elementMouseEnterHandler = () => { + if (editor.isEditable) { + wrapper.appendChild(leftResizeHandle); + wrapper.appendChild(rightResizeHandle); + } + }; + // Hides the resize handles when the cursor leaves the element, unless the + // cursor moves to one of the resize handles. + const elementMouseLeaveHandler = (event: MouseEvent) => { + if ( + event.relatedTarget === leftResizeHandle || + event.relatedTarget === rightResizeHandle + ) { + return; + } + + if (resizeParams) { + return; + } + + if ( + editor.isEditable && + wrapper.contains(leftResizeHandle) && + wrapper.contains(rightResizeHandle) + ) { + wrapper.removeChild(leftResizeHandle); + wrapper.removeChild(rightResizeHandle); + } + }; + + // Sets the resize params, allowing the user to begin resizing the element by + // moving the cursor left or right. + const leftResizeHandleMouseDownHandler = (event: MouseEvent) => { + event.preventDefault(); + + wrapper.appendChild(leftResizeHandle); + wrapper.appendChild(rightResizeHandle); + + resizeParams = { + handleUsed: "left", + initialWidth: block.props.previewWidth!, + initialClientX: event.clientX, + }; + }; + const rightResizeHandleMouseDownHandler = (event: MouseEvent) => { + event.preventDefault(); + + wrapper.appendChild(leftResizeHandle); + wrapper.appendChild(rightResizeHandle); + + resizeParams = { + handleUsed: "right", + initialWidth: block.props.previewWidth!, + initialClientX: event.clientX, + }; + }; + + wrapper.appendChild(element); + + window.addEventListener("mousemove", windowMouseMoveHandler); + window.addEventListener("mouseup", windowMouseUpHandler); + element.addEventListener("mouseenter", elementMouseEnterHandler); + element.addEventListener("mouseleave", elementMouseLeaveHandler); + leftResizeHandle.addEventListener( + "mousedown", + leftResizeHandleMouseDownHandler + ); + rightResizeHandle.addEventListener( + "mousedown", + rightResizeHandleMouseDownHandler + ); + + return { + dom: wrapper, + destroy: () => { + window.removeEventListener("mousemove", windowMouseMoveHandler); + window.removeEventListener("mouseup", windowMouseUpHandler); + leftResizeHandle.removeEventListener( + "mousedown", + leftResizeHandleMouseDownHandler + ); + rightResizeHandle.removeEventListener( + "mousedown", + rightResizeHandleMouseDownHandler + ); + }, + }; +}; diff --git a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts similarity index 62% rename from packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts rename to packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts index 97960253e0..32471dc031 100644 --- a/packages/core/src/blocks/FileBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts @@ -1,23 +1,24 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockFromConfig, + createBlockSpec, FileBlockConfig, - PropSchema, Props, - createBlockSpec, + PropSchema, } from "../../schema"; import { defaultProps } from "../defaultProps"; -import { renderWithResizeHandles } from "./extensions/utils/renderWithResizeHandles"; import { - createFileAndCaptionDOM, - createFileIconAndNameDOM, - createFilePlaceholderDOM, - parseFigure, - toExternalHTMLWithCaption, -} from "./fileBlockHelpers"; - -export const propSchema = { + createAddFileButton, + createDefaultFilePreview, + createFigureWithCaption, + createFileAndCaptionWrapper, + createResizeHandlesWrapper, + parseFigureElement, +} from "../FileBlockContent/fileBlockHelpers"; +import { parseImageElement } from "./imageBlockHelpers"; + +export const imagePropSchema = { textAlignment: defaultProps.textAlignment, backgroundColor: defaultProps.backgroundColor, // File name. @@ -44,36 +45,39 @@ export const propSchema = { export const imageBlockConfig = { type: "image" as const, - propSchema, + propSchema: imagePropSchema, content: "none", isFileBlock: true, } satisfies FileBlockConfig; -export const fileRender = ( +export const imageRender = ( block: BlockFromConfig, editor: BlockNoteEditor ) => { - // Wrapper element to set the file alignment, contains both file/file - // upload dashboard and caption. const wrapper = document.createElement("div"); wrapper.className = "bn-file-block-content-wrapper"; - // File element. - if (block.props.url === "") { - // TODO: pass image related things - const placeholder = createFilePlaceholderDOM(block, editor); - wrapper.appendChild(placeholder.dom); + const fileBlockImageIcon = document.createElement("div"); + fileBlockImageIcon.innerHTML = + ''; + const addImageButton = createAddFileButton( + block, + editor, + "Add image", + fileBlockImageIcon.firstElementChild as HTMLElement + ); + wrapper.appendChild(addImageButton.dom); return { dom: wrapper, destroy: () => { - placeholder?.destroy?.(); + addImageButton?.destroy?.(); }, }; } else if (!block.props.showPreview) { - const file = createFileIconAndNameDOM(block).dom; - const element = createFileAndCaptionDOM(block, editor, file); + const file = createDefaultFilePreview(block).dom; + const element = createFileAndCaptionWrapper(block, file); return { dom: element.dom, @@ -90,7 +94,7 @@ export const fileRender = ( editor.domElement.firstElementChild!.clientWidth ); - const file = renderWithResizeHandles( + const file = createResizeHandlesWrapper( block, editor, image, @@ -98,7 +102,7 @@ export const fileRender = ( (width) => (image.width = width) ); - const element = createFileAndCaptionDOM(block, editor, file.dom); + const element = createFileAndCaptionWrapper(block, file.dom); wrapper.appendChild(element.dom); return { @@ -108,22 +112,15 @@ export const fileRender = ( } }; -export const parseImage = (imageElement: HTMLImageElement) => { - const url = imageElement.src || undefined; - const previewWidth = imageElement.width || undefined; - - return { url, previewWidth }; -}; - export const imageParse = ( element: HTMLElement ): Partial> | undefined => { if (element.tagName === "IMG") { - return parseImage(element as HTMLImageElement); + return parseImageElement(element as HTMLImageElement); } if (element.tagName === "FIGURE") { - const parsedFigure = parseFigure(element, "img"); + const parsedFigure = parseFigureElement(element, "img"); if (!parsedFigure) { return undefined; } @@ -131,7 +128,7 @@ export const imageParse = ( const { targetElement, caption } = parsedFigure; return { - ...parseImage(targetElement as HTMLImageElement), + ...parseImageElement(targetElement as HTMLImageElement), caption, }; } @@ -155,7 +152,7 @@ export const imageToExternalHTML = ( image.src = block.props.url; if (block.props.caption) { - return toExternalHTMLWithCaption(image, block.props.caption); + return createFigureWithCaption(image, block.props.caption); } return { @@ -164,7 +161,7 @@ export const imageToExternalHTML = ( }; export const ImageBlock = createBlockSpec(imageBlockConfig, { - render: fileRender, + render: imageRender, parse: imageParse, toExternalHTML: imageToExternalHTML, }); diff --git a/packages/core/src/blocks/ImageBlockContent/imageBlockHelpers.ts b/packages/core/src/blocks/ImageBlockContent/imageBlockHelpers.ts new file mode 100644 index 0000000000..d225b9daa3 --- /dev/null +++ b/packages/core/src/blocks/ImageBlockContent/imageBlockHelpers.ts @@ -0,0 +1,6 @@ +export const parseImageElement = (imageElement: HTMLImageElement) => { + const url = imageElement.src || undefined; + const previewWidth = imageElement.width || undefined; + + return { url, previewWidth }; +}; diff --git a/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts b/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts new file mode 100644 index 0000000000..32a99c8bde --- /dev/null +++ b/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts @@ -0,0 +1,167 @@ +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { + BlockFromConfig, + createBlockSpec, + FileBlockConfig, + Props, + PropSchema, +} from "../../schema"; +import { defaultProps } from "../defaultProps"; + +import { + createAddFileButton, + createDefaultFilePreview, + createFigureWithCaption, + createFileAndCaptionWrapper, + createResizeHandlesWrapper, + parseFigureElement, +} from "../FileBlockContent/fileBlockHelpers"; +import { parseVideoElement } from "./videoBlockHelpers"; + +export const videoPropSchema = { + 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: 512, + }, +} satisfies PropSchema; + +export const videoBlockConfig = { + type: "video" as const, + propSchema: videoPropSchema, + content: "none", + isFileBlock: true, +} satisfies FileBlockConfig; + +export const videoRender = ( + block: BlockFromConfig, + editor: BlockNoteEditor +) => { + const wrapper = document.createElement("div"); + wrapper.className = "bn-file-block-content-wrapper"; + + if (block.props.url === "") { + const fileBlockVideoIcon = document.createElement("div"); + fileBlockVideoIcon.innerHTML = + ''; + const addVideoButton = createAddFileButton( + block, + editor, + "Add video", + fileBlockVideoIcon.firstElementChild as HTMLElement + ); + wrapper.appendChild(addVideoButton.dom); + + return { + dom: wrapper, + destroy: () => { + addVideoButton?.destroy?.(); + }, + }; + } else if (!block.props.showPreview) { + const file = createDefaultFilePreview(block).dom; + const element = createFileAndCaptionWrapper(block, file); + + return { + dom: element.dom, + }; + } else { + const video = document.createElement("video"); + video.className = "bn-visual-media"; + video.src = block.props.url; + video.controls = true; + video.contentEditable = "false"; + video.draggable = false; + video.width = Math.min( + block.props.previewWidth, + editor.domElement.firstElementChild!.clientWidth + ); + + const file = createResizeHandlesWrapper( + block, + editor, + video, + () => video.width, + (width) => (video.width = width) + ); + + const element = createFileAndCaptionWrapper(block, file.dom); + wrapper.appendChild(element.dom); + + return { + dom: wrapper, + destroy: file.destroy, + }; + } +}; + +export const videoParse = ( + element: HTMLElement +): Partial> | undefined => { + if (element.tagName === "VIDEO") { + return parseVideoElement(element as HTMLVideoElement); + } + + if (element.tagName === "FIGURE") { + const parsedFigure = parseFigureElement(element, "video"); + if (!parsedFigure) { + return undefined; + } + + const { targetElement, caption } = parsedFigure; + + return { + ...parseVideoElement(targetElement as HTMLVideoElement), + caption, + }; + } + + return undefined; +}; + +export const videoToExternalHTML = ( + block: BlockFromConfig +) => { + if (!block.props.url) { + const div = document.createElement("p"); + div.innerHTML = "Add video"; + + return { + dom: div, + }; + } + + const video = document.createElement("video"); + video.src = block.props.url; + + if (block.props.caption) { + return createFigureWithCaption(video, block.props.caption); + } + + return { + dom: video, + }; +}; + +export const VideoBlock = createBlockSpec(videoBlockConfig, { + render: videoRender, + parse: videoParse, + toExternalHTML: videoToExternalHTML, +}); diff --git a/packages/core/src/blocks/VideoBlockContent/videoBlockHelpers.ts b/packages/core/src/blocks/VideoBlockContent/videoBlockHelpers.ts new file mode 100644 index 0000000000..4b11481d48 --- /dev/null +++ b/packages/core/src/blocks/VideoBlockContent/videoBlockHelpers.ts @@ -0,0 +1,6 @@ +export const parseVideoElement = (videoElement: HTMLVideoElement) => { + const url = videoElement.src || undefined; + const previewWidth = videoElement.width || undefined; + + return { url, previewWidth }; +}; diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 29064a1513..8bde3a6077 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -20,12 +20,14 @@ import { getStyleSchemaFromSpecs, } from "../schema"; import { FileBlock } from "./FileBlockContent/FileBlockContent"; -import { ImageBlock } from "./FileBlockContent/ImageBlockContent"; +import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { Heading } from "./HeadingBlockContent/HeadingBlockContent"; import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { Paragraph } from "./ParagraphBlockContent/ParagraphBlockContent"; import { Table } from "./TableBlockContent/TableBlockContent"; +import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; +import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -34,6 +36,8 @@ export const defaultBlockSpecs = { numberedListItem: NumberedListItem, file: FileBlock, image: ImageBlock, + video: VideoBlock, + audio: AudioBlock, table: Table, } satisfies BlockSpecs; diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index c9a9d5a46c..8af8254973 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -246,12 +246,11 @@ NESTED BLOCKS width: 100%; } -/* // TODO */ -.bn-file-block-content-wrapper:not([data-url]) { +[data-file-block]:not([data-url]) { width: 100%; } -.bn-file-block-content-wrapper .bn-add-file-button { +.bn-add-file-button { display: flex; flex-direction: row; align-items: center; @@ -263,42 +262,42 @@ NESTED BLOCKS width: 100%; } -.bn-file-block-content-wrapper .bn-add-file-button:hover { +.bn-add-file-button:hover { background-color: gainsboro; } -.bn-file-block-content-wrapper .bn-add-file-button-icon { +.bn-add-file-button-icon { width: 24px; height: 24px; } -.bn-file-block-content-wrapper .bn-add-file-button-text { +.bn-add-file-button-text { color: black; } -.bn-file-block-content-wrapper .bn-file-and-caption-wrapper { +.bn-file-and-caption-wrapper { display: flex; flex-direction: column; border-radius: 4px; } -.bn-file-block-content-wrapper .bn-file-default-preview { +.bn-file-default-preview { align-items: center; display: flex; flex-direction: row; gap: 4px; } -.bn-file-block-content-wrapper .bn-file-default-preview-icon { +.bn-file-default-preview-icon { width: 24px; height: 24px; } -.bn-file-block-content-wrapper .bn-file-default-preview-name { +.bn-file-default-preview-name { /*font-size: 0.8em;*/ } -.bn-file-block-content-wrapper .bn-visual-media-wrapper { +.bn-visual-media-wrapper { display: flex; flex-direction: row; align-items: center; @@ -306,12 +305,12 @@ NESTED BLOCKS width: fit-content; } -.bn-file-block-content-wrapper .bn-visual-media { +.bn-visual-media { border-radius: 4px; max-width: 100%; } -.bn-file-block-content-wrapper .bn-visual-media-resize-handle { +.bn-visual-media-resize-handle { position: absolute; width: 8px; height: 30px; @@ -321,16 +320,16 @@ NESTED BLOCKS cursor: ew-resize; } -.bn-file-block-content-wrapper .bn-audio { +[data-content-type="audio"], .bn-audio { width: 100%; } -.bn-file-block-content-wrapper .bn-file-caption { +.bn-file-caption { font-size: 0.8em; padding-block: 4px; } -.bn-file-block-content-wrapper .bn-file-caption:empty { +.bn-file-caption:empty { padding-block: 0; } diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 8ef05e7fb1..eb41dddcdd 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -200,6 +200,44 @@ export function getDefaultSlashMenuItems< }); } + if (checkDefaultBlockTypeInSchema("video", editor)) { + items.push({ + onItemClick: () => { + const insertedBlock = insertOrUpdateBlock(editor, { + type: "video", + }); + + // Immediately open the file toolbar + editor.prosemirrorView.dispatch( + editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + block: insertedBlock, + }) + ); + }, + key: "video", + ...editor.dictionary.slash_menu.video, + }); + } + + if (checkDefaultBlockTypeInSchema("audio", editor)) { + items.push({ + onItemClick: () => { + const insertedBlock = insertOrUpdateBlock(editor, { + type: "audio", + }); + + // Immediately open the file toolbar + editor.prosemirrorView.dispatch( + editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + block: insertedBlock, + }) + ); + }, + key: "audio", + ...editor.dictionary.slash_menu.audio, + }); + } + if (checkDefaultBlockTypeInSchema("file", editor)) { items.push({ onItemClick: () => { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 798e49a79f..88b0f6186a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,9 @@ export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; export * from "./blocks/FileBlockContent/FileBlockContent"; -export * from "./blocks/FileBlockContent/ImageBlockContent"; +export * from "./blocks/ImageBlockContent/ImageBlockContent"; +export * from "./blocks/VideoBlockContent/VideoBlockContent"; +export * from "./blocks/AudioBlockContent/AudioBlockContent"; export * from "./blocks/FileBlockContent/fileBlockHelpers"; export * from "./blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; @@ -33,3 +35,4 @@ export * from "./extensions/UniqueID/UniqueID"; export * from "./i18n/dictionary"; export { UnreachableCaseError, assertEmpty } from "./util/typescript"; export { locales }; +export { parseImageElement } from "./blocks/ImageBlockContent/imageBlockHelpers"; diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index eee2ac6788..248445a4c2 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -193,6 +193,7 @@ export function createBlockSpec< block.type, block.props, blockConfig.propSchema, + blockConfig.isFileBlock, blockContentDOMAttributes ); }, diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index 7ab8b1d4c5..9e5722e6a8 100644 --- a/packages/core/src/schema/blocks/internal.ts +++ b/packages/core/src/schema/blocks/internal.ts @@ -139,6 +139,7 @@ export function wrapInBlockStructure< blockType: BType, blockProps: Props, propSchema: PSchema, + isFileBlock = false, domAttributes?: Record ): { dom: HTMLElement; @@ -171,6 +172,10 @@ export function wrapInBlockStructure< blockContent.setAttribute(camelToDataKebab(prop), value); } } + // Adds file block attribute + if (isFileBlock) { + blockContent.setAttribute("data-file-block", ""); + } blockContent.appendChild(element.dom); From d67ab4e87d1bfff4cba8308b571f3db9166df60f Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 20 May 2024 13:12:27 +0200 Subject: [PATCH 7/7] Refactored react package --- examples/01-basic/testing/App.tsx | 9 +- .../src/api/testUtil/cases/customBlocks.ts | 32 +++-- .../src/api/testUtil/cases/defaultSchema.ts | 14 +- .../FileBlockContent/FileBlockContent.ts | 10 +- .../FileBlockContent/fileBlockHelpers.ts | 3 +- .../ImageBlockContent/ImageBlockContent.ts | 1 + packages/core/src/i18n/locales/en.ts | 15 +- packages/core/src/i18n/locales/fr.ts | 16 ++- packages/core/src/i18n/locales/nl.ts | 16 ++- packages/core/src/i18n/locales/zh.ts | 16 ++- .../AudioBlockContent/AudioBlockContent.tsx | 79 +++++++++++ .../FileBlockContent/FileBlockContent.tsx | 54 ++++++++ .../FileBlockContent/fileBlockHelpers.tsx} | 131 +++++++++++++++--- .../ImageBlockContent/ImageBlockContent.tsx | 98 +++++++++++++ .../VideoBlockContent/VideoBlockContent.tsx | 93 +++++++++++++ .../components/FileBlock/FileBlockContent.tsx | 102 -------------- .../FileBlock/ImageBlockContent.tsx | 69 --------- .../extensions/reactAudioFileExtension.tsx | 61 -------- .../extensions/reactImageFileExtension.tsx | 90 ------------ .../extensions/reactVideoFileExtension.tsx | 75 ---------- .../components/FileBlock/fileBlockHelpers.tsx | 76 ---------- .../DefaultButtons/FileCaptionButton.tsx | 1 - .../DefaultButtons/FileRenameButton.tsx | 1 - packages/react/src/index.ts | 6 +- packages/react/src/schema/ReactBlockSpec.tsx | 5 +- .../src/test/testCases/customReactBlocks.tsx | 105 ++++++++++++-- 26 files changed, 625 insertions(+), 553 deletions(-) create mode 100644 packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx create mode 100644 packages/react/src/blocks/FileBlockContent/FileBlockContent.tsx rename packages/react/src/{components/FileBlock/extensions/utils/ResizeHandlesWrapper.tsx => blocks/FileBlockContent/fileBlockHelpers.tsx} (61%) create mode 100644 packages/react/src/blocks/ImageBlockContent/ImageBlockContent.tsx create mode 100644 packages/react/src/blocks/VideoBlockContent/VideoBlockContent.tsx delete mode 100644 packages/react/src/components/FileBlock/FileBlockContent.tsx delete mode 100644 packages/react/src/components/FileBlock/ImageBlockContent.tsx delete mode 100644 packages/react/src/components/FileBlock/extensions/reactAudioFileExtension.tsx delete mode 100644 packages/react/src/components/FileBlock/extensions/reactImageFileExtension.tsx delete mode 100644 packages/react/src/components/FileBlock/extensions/reactVideoFileExtension.tsx delete mode 100644 packages/react/src/components/FileBlock/fileBlockHelpers.tsx diff --git a/examples/01-basic/testing/App.tsx b/examples/01-basic/testing/App.tsx index c00a167616..da173bab19 100644 --- a/examples/01-basic/testing/App.tsx +++ b/examples/01-basic/testing/App.tsx @@ -6,12 +6,19 @@ import { import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -import { ReactImageBlock, useCreateBlockNote } from "@blocknote/react"; +import { + ReactAudioBlock, + ReactImageBlock, + ReactVideoBlock, + useCreateBlockNote, +} from "@blocknote/react"; const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, image: ReactImageBlock, + video: ReactVideoBlock, + audio: ReactAudioBlock, }, }); diff --git a/packages/core/src/api/testUtil/cases/customBlocks.ts b/packages/core/src/api/testUtil/cases/customBlocks.ts index 0466f9f09c..9554a82b10 100644 --- a/packages/core/src/api/testUtil/cases/customBlocks.ts +++ b/packages/core/src/api/testUtil/cases/customBlocks.ts @@ -1,7 +1,5 @@ import { EditorTestCases } from "../index"; -import { filePropSchema } from "../../../blocks/FileBlockContent/fileBlockConfig"; -import { fileRender } from "../../../blocks/FileBlockContent/fileBlockHelpers"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; import { DefaultInlineContentSchema, @@ -12,18 +10,22 @@ import { defaultProps } from "../../../blocks/defaultProps"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; import { BlockNoteSchema } from "../../../editor/BlockNoteSchema"; import { createBlockSpec } from "../../../schema"; +import { + imagePropSchema, + imageRender, +} from "../../../blocks/ImageBlockContent/ImageBlockContent"; -// This is a modified version of the default file block that does not implement +// This is a modified version of the default image block that does not implement // a `toExternalHTML` function. It's used to test if the custom serializer by // default serializes custom blocks using their `render` function. -const SimpleFile = createBlockSpec( +const SimpleImage = createBlockSpec( { - type: "simpleFile", - propSchema: filePropSchema, + type: "simpleImage", + propSchema: imagePropSchema, content: "none", }, { - render: (block, editor) => fileRender(block as any, editor as any), + render: (block, editor) => imageRender(block as any, editor as any), } ); @@ -77,7 +79,7 @@ const SimpleCustomParagraph = createBlockSpec( const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, - simpleFile: SimpleFile, + simpleImage: SimpleImage, customParagraph: CustomParagraph, simpleCustomParagraph: SimpleCustomParagraph, }, @@ -97,18 +99,18 @@ export const customBlocksTestCases: EditorTestCases< }, documents: [ { - name: "simpleFile/button", + name: "simpleImage/button", blocks: [ { - type: "simpleFile", + type: "simpleImage", }, ], }, { - name: "simpleFile/basic", + name: "simpleImage/basic", blocks: [ { - type: "simpleFile", + type: "simpleImage", props: { url: "exampleURL", caption: "Caption", @@ -118,10 +120,10 @@ export const customBlocksTestCases: EditorTestCases< ], }, { - name: "simpleFile/nested", + name: "simpleImage/nested", blocks: [ { - type: "simpleFile", + type: "simpleImage", props: { url: "exampleURL", caption: "Caption", @@ -129,7 +131,7 @@ export const customBlocksTestCases: EditorTestCases< }, children: [ { - type: "simpleFile", + type: "simpleImage", props: { url: "exampleURL", caption: "Caption", diff --git a/packages/core/src/api/testUtil/cases/defaultSchema.ts b/packages/core/src/api/testUtil/cases/defaultSchema.ts index a5d13dbcee..b7b59747ac 100644 --- a/packages/core/src/api/testUtil/cases/defaultSchema.ts +++ b/packages/core/src/api/testUtil/cases/defaultSchema.ts @@ -143,10 +143,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/button", blocks: [ { - type: "file", - props: { - fileType: "image", - }, + type: "image", }, ], }, @@ -154,9 +151,8 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/basic", blocks: [ { - type: "file", + type: "image", props: { - fileType: "image", url: "exampleURL", caption: "Caption", previewWidth: 256, @@ -168,18 +164,16 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/nested", blocks: [ { - type: "file", + type: "image", props: { - fileType: "image", url: "exampleURL", caption: "Caption", previewWidth: 256, }, children: [ { - type: "file", + type: "image", props: { - fileType: "image", url: "exampleURL", caption: "Caption", previewWidth: 256, diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index f352228adb..e15d649d9c 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -100,20 +100,24 @@ export const fileToExternalHTML = ( }; } - const wrapper = document.createElement("div"); const fileSrcLink = document.createElement("a"); fileSrcLink.href = block.props.url; fileSrcLink.innerText = block.props.name; - wrapper.appendChild(fileSrcLink); if (block.props.caption) { + const wrapper = document.createElement("div"); const fileCaption = document.createElement("p"); fileCaption.innerText = block.props.caption; + wrapper.appendChild(fileSrcLink); wrapper.appendChild(fileCaption); + + return { + dom: wrapper, + }; } return { - dom: wrapper, + dom: fileSrcLink, }; }; diff --git a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts index 7a2391b708..7ddc865166 100644 --- a/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts +++ b/packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts @@ -66,7 +66,8 @@ export const createAddFileButton = ( const addFileButtonText = document.createElement("p"); addFileButtonText.className = "bn-add-file-button-text"; - addFileButtonText.innerHTML = buttonText || "Add file"; + addFileButtonText.innerHTML = + buttonText || editor.dictionary.file_blocks.file.add_button_text; // Prevents focus from moving to the button. const addFileButtonMouseDownHandler = (event: MouseEvent) => { diff --git a/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts index 32471dc031..06fa6b3553 100644 --- a/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts @@ -150,6 +150,7 @@ export const imageToExternalHTML = ( const image = document.createElement("img"); image.src = block.props.url; + image.alt = block.props.caption || "BlockNote image"; if (block.props.caption) { return createFigureWithCaption(image, block.props.caption); diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 46602aee2a..1a48f18a40 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -103,8 +103,19 @@ export const en = { bulletListItem: "List", numberedListItem: "List", }, - file: { - button_add_file_text: "Add file", + file_blocks: { + image: { + add_button_text: "Add image", + }, + video: { + add_button_text: "Add video", + }, + audio: { + add_button_text: "Add audio", + }, + file: { + add_button_text: "Add file", + }, }, // from react package: side_menu: { diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index 1b18bb823f..0ac4b31c88 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -107,9 +107,19 @@ export const fr: Dictionary = { numberedListItem: "Liste", }, // TODO - file: { - button_add_text: "Add", - button_default_file_type_text: "file", + file_blocks: { + image: { + add_button_text: "Add image", + }, + video: { + add_button_text: "Add video", + }, + audio: { + add_button_text: "Add audio", + }, + file: { + add_button_text: "Add file", + }, }, // from react package: side_menu: { diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index 3f0e40957f..2da253e7ba 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -107,9 +107,19 @@ export const nl: Dictionary = { numberedListItem: "Lijst", }, // TODO - file: { - button_add_text: "Add", - button_default_file_type_text: "file", + file_blocks: { + image: { + add_button_text: "Add image", + }, + video: { + add_button_text: "Add video", + }, + audio: { + add_button_text: "Add audio", + }, + file: { + add_button_text: "Add file", + }, }, // from react package: side_menu: { diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index d5d2c8b607..80ce26fb81 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -122,9 +122,19 @@ export const zh: Dictionary = { numberedListItem: "列表", }, // TODO - file: { - button_add_text: "Add", - button_default_file_type_text: "file", + file_blocks: { + image: { + add_button_text: "Add image", + }, + video: { + add_button_text: "Add video", + }, + audio: { + add_button_text: "Add audio", + }, + file: { + add_button_text: "Add file", + }, }, // from react package: side_menu: { diff --git a/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx b/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx new file mode 100644 index 0000000000..1decbe204e --- /dev/null +++ b/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx @@ -0,0 +1,79 @@ +import { FileBlockConfig, audioBlockConfig, audioParse } from "@blocknote/core"; +import { RiVolumeUpFill } from "react-icons/ri"; + +import { + createReactBlockSpec, + ReactCustomBlockRenderProps, +} from "../../schema/ReactBlockSpec"; +import { + FileAndCaptionWrapper, + AddFileButton, + DefaultFilePreview, + FigureWithCaption, +} from "../FileBlockContent/fileBlockHelpers"; + +export const AudioPreview = ( + props: Omit< + ReactCustomBlockRenderProps, + "contentRef" + > +) => ( +