diff --git a/examples/01-basic/04-default-blocks/App.tsx b/examples/01-basic/04-default-blocks/App.tsx index 992a52d21..01979e853 100644 --- a/examples/01-basic/04-default-blocks/App.tsx +++ b/examples/01-basic/04-default-blocks/App.tsx @@ -1,16 +1,45 @@ +import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { useCreateBlockNote } from "@blocknote/react"; +// import { ToggleBlock } from "@blocknote/core"; +import { ReactToggleBlock } from "@blocknote/react"; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ + schema: BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + // toggle: ToggleBlock, + toggle: ReactToggleBlock, + }, + }), initialContent: [ { type: "paragraph", content: "Welcome to this demo!", }, + { + type: "toggle", + content: "Toggle", + children: [ + { + type: "paragraph", + content: "Child 1", + }, + { + type: "paragraph", + content: "Child 2", + }, + { + type: "paragraph", + content: "Child 3", + }, + ], + }, { type: "paragraph", }, diff --git a/packages/core/src/blocks/ToggleBlockContent/ToggleBlockContent.ts b/packages/core/src/blocks/ToggleBlockContent/ToggleBlockContent.ts new file mode 100644 index 000000000..25f8e5dc9 --- /dev/null +++ b/packages/core/src/blocks/ToggleBlockContent/ToggleBlockContent.ts @@ -0,0 +1,124 @@ +import { + BlockConfig, + BlockFromConfig, + createBlockSpec, + PropSchema, +} from "../../schema/index.js"; + +import { defaultProps } from "../defaultProps.js"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; + +export const togglePropSchema = { + ...defaultProps, +} satisfies PropSchema; + +export const toggleBlockConfig = { + type: "toggle" as const, + propSchema: togglePropSchema, + content: "inline", +} satisfies BlockConfig; + +export const toggleBlockRender = ( + block: BlockFromConfig, + editor: BlockNoteEditor, +) => { + const dom = document.createElement("div"); + + const toggleWrapper = document.createElement("div"); + toggleWrapper.className = "bn-toggle-wrapper"; + toggleWrapper.setAttribute("data-show-children", "true"); + + const toggleButton = document.createElement("button"); + toggleButton.className = "bn-toggle-button"; + toggleButton.innerHTML = + ''; + const toggleButtonMouseDown = (event: MouseEvent) => event.preventDefault(); + toggleButton.addEventListener("mousedown", toggleButtonMouseDown); + const toggleButtonOnClick = () => { + if (toggleWrapper.getAttribute("data-show-children") === "true") { + toggleWrapper.setAttribute("data-show-children", "false"); + } else { + toggleWrapper.setAttribute("data-show-children", "true"); + } + }; + toggleButton.addEventListener("click", toggleButtonOnClick); + + const contentDOM = document.createElement("p"); + + toggleWrapper.appendChild(toggleButton); + toggleWrapper.appendChild(contentDOM); + + const toggleAddBlockButton = document.createElement("button"); + toggleAddBlockButton.className = "bn-toggle-add-block-button"; + toggleAddBlockButton.textContent = "Empty toggle. Click to add a block."; + const toggleAddBlockButtonMouseDown = (event: MouseEvent) => + event.preventDefault(); + toggleAddBlockButton.addEventListener( + "mousedown", + toggleAddBlockButtonMouseDown, + ); + const toggleAddBlockButtonOnClick = () => { + editor.transact(() => { + const updatedBlock = editor.updateBlock(block, { + // Single empty block with default type. + children: [{}], + }); + editor.setTextCursorPosition(updatedBlock.children[0].id, "end"); + editor.focus(); + }); + }; + toggleAddBlockButton.addEventListener("click", toggleAddBlockButtonOnClick); + + dom.appendChild(toggleWrapper); + if ( + block.children.length === 0 && + toggleWrapper.getAttribute("data-show-children") === "true" + ) { + dom.appendChild(toggleAddBlockButton); + } + + // Hack to force a re-render if the block has changed from having children to + // not having children or vice versa. + const onEditorChange = editor.onChange(() => { + const actualBlock = editor.getBlock(block); + if (!actualBlock) { + return; + } + + if ( + actualBlock.children.length === 0 && + toggleAddBlockButton.parentElement === null + ) { + dom.appendChild(toggleAddBlockButton); + } + + if ( + actualBlock.children.length > 0 && + toggleAddBlockButton.parentElement === dom + ) { + dom.removeChild(toggleAddBlockButton); + } + }); + + return { + dom, + contentDOM, + destroy: () => { + toggleButton.removeEventListener("mousedown", toggleButtonMouseDown); + toggleButton.removeEventListener("click", toggleButtonOnClick); + toggleAddBlockButton.removeEventListener( + "mousedown", + toggleAddBlockButtonMouseDown, + ); + toggleAddBlockButton.removeEventListener( + "click", + toggleAddBlockButtonOnClick, + ); + onEditorChange?.(); + }, + }; +}; + +export const ToggleBlock = createBlockSpec(toggleBlockConfig, { + render: toggleBlockRender, +}); diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 40196d861..c96ba8277 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -427,6 +427,57 @@ NESTED BLOCKS width: 100%; } +/* Toggle blocks */ +.bn-block:has( + > .bn-block-content > div > .bn-toggle-wrapper[data-show-children="false"] + ) + > .bn-block-group, +.bn-block:has( + > .react-renderer + > .bn-block-content + > div + > .bn-toggle-wrapper[data-show-children="false"] + ) + > .bn-block-group { + display: none; +} + +.bn-toggle-wrapper { + display: flex; + align-items: center; +} + +.bn-toggle-button, +.bn-toggle-add-block-button { + cursor: pointer; + display: flex; + align-items: center; + padding: 0; + border-radius: var; + background: none; + border: none; + border-radius: 4px; + color: var(--bn-colors-editor-text); + user-select: none; +} + +.bn-toggle-button:hover, +.bn-toggle-add-block-button:hover { + background-color: var(--bn-colors-hovered-background); +} + +.bn-toggle-add-block-button { + font-size: 16px; + color: var(--bn-colors-side-menu); + font-weight: normal; + margin-left: 1.5em; + padding: 2px; +} + +.bn-toggle-wrapper[data-show-children="true"] .bn-toggle-button { + transform: rotate(90deg); +} + /* Block-specific styles */ [data-content-type="audio"] > .bn-file-block-content-wrapper, .bn-audio { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be7060c85..eac14418b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,6 +19,7 @@ export * from "./blocks/ImageBlockContent/ImageBlockContent.js"; export * from "./blocks/PageBreakBlockContent/PageBreakBlockContent.js"; export * from "./blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.js"; export * from "./blocks/PageBreakBlockContent/schema.js"; +export * from "./blocks/ToggleBlockContent/ToggleBlockContent.js"; export { EMPTY_CELL_HEIGHT, EMPTY_CELL_WIDTH, diff --git a/packages/react/src/blocks/ToggleBlockContent/ToggleBlockContent.tsx b/packages/react/src/blocks/ToggleBlockContent/ToggleBlockContent.tsx new file mode 100644 index 000000000..9898af197 --- /dev/null +++ b/packages/react/src/blocks/ToggleBlockContent/ToggleBlockContent.tsx @@ -0,0 +1,69 @@ +import { toggleBlockConfig } from "@blocknote/core"; +import { useState } from "react"; + +import { + createReactBlockSpec, + ReactCustomBlockRenderProps, +} from "../../schema/ReactBlockSpec.js"; +import { useEditorChange } from "../../hooks/useEditorChange.js"; + +export const ToggleBlock = ( + props: ReactCustomBlockRenderProps, +) => { + const { block, editor, contentRef } = props; + + const [showChildren, setShowChildren] = useState(true); + const [hasChildren, setHasChildren] = useState(block.children.length > 0); + + useEditorChange(() => { + const actualBlock = editor.getBlock(block); + + if (actualBlock?.children.length === 0) { + setHasChildren(false); + } else { + setHasChildren(true); + } + }); + + return ( +
+
+ +

+

+ {showChildren && !hasChildren && ( + + )} +
+ ); +}; + +export const ReactToggleBlock = createReactBlockSpec(toggleBlockConfig, { + render: ToggleBlock, +}); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1d6c0a914..0bff8fd94 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -16,6 +16,7 @@ export * from "./blocks/FileBlockContent/helpers/toExternalHTML/LinkWithCaption. export * from "./blocks/FileBlockContent/useResolveUrl.js"; export * from "./blocks/ImageBlockContent/ImageBlockContent.js"; export * from "./blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.js"; +export * from "./blocks/ToggleBlockContent/ToggleBlockContent.js"; export * from "./blocks/VideoBlockContent/VideoBlockContent.js"; export * from "./components/FormattingToolbar/DefaultButtons/AddCommentButton.js";