Skip to content

feat: Toggle blocks #1707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions examples/01-basic/04-default-blocks/App.tsx
Original file line number Diff line number Diff line change
@@ -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",
},
Expand Down
124 changes: 124 additions & 0 deletions packages/core/src/blocks/ToggleBlockContent/ToggleBlockContent.ts
Original file line number Diff line number Diff line change
@@ -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<typeof toggleBlockConfig, any, any>,
editor: BlockNoteEditor<any, any, any>,
) => {
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 =
'<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentcolor"><path d="M472-480 332-620q-18-18-18-44t18-44q18-18 44-18t44 18l183 183q9 9 14 21t5 24q0 12-5 24t-14 21L420-252q-18 18-44 18t-44-18q-18-18-18-44t18-44l140-140Z"/></svg>';
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,
});
51 changes: 51 additions & 0 deletions packages/core/src/editor/Block.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof toggleBlockConfig, any, any>,
) => {
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 (
<div>
<div className="bn-toggle-wrapper" data-show-children={showChildren}>
<button
className="bn-toggle-button"
onMouseDown={(event) => event.preventDefault()}
onClick={() => setShowChildren((showChildren) => !showChildren)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 -960 960 960"
width="1em"
fill="currentcolor"
>
<path d="M472-480 332-620q-18-18-18-44t18-44q18-18 44-18t44 18l183 183q9 9 14 21t5 24q0 12-5 24t-14 21L420-252q-18 18-44 18t-44-18q-18-18-18-44t18-44l140-140Z" />
</svg>
</button>
<p ref={contentRef} />
</div>
{showChildren && !hasChildren && (
<button
className="bn-toggle-add-block-button"
onClick={() => {
const updatedBlock = editor.updateBlock(block, {
// Single empty block with default type.
children: [{}],
});
editor.setTextCursorPosition(updatedBlock.children[0].id, "end");
editor.focus();
}}
>
Empty toggle. Click to add a block.
</button>
)}
</div>
);
};

export const ReactToggleBlock = createReactBlockSpec(toggleBlockConfig, {
render: ToggleBlock,
});
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading