@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
186 lines (164 loc) • 6.72 kB
text/typescript
import { ViewMutationRecord } from "@tiptap/pm/view";
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import { Block } from "../defaultBlocks.js";
type ToggledState = {
set: (block: Block<any, any, any>, isToggled: boolean) => void;
get: (block: Block<any, any, any>) => boolean;
};
export const defaultToggledState: ToggledState = {
set: (block, isToggled: boolean) =>
window.localStorage.setItem(
`toggle-${block.id}`,
isToggled ? "true" : "false",
),
get: (block) => window.localStorage.getItem(`toggle-${block.id}`) === "true",
};
export const createToggleWrapper = (
block: Block<any, any, any>,
editor: BlockNoteEditor<any, any, any>,
renderedElement: HTMLElement,
toggledState: ToggledState = defaultToggledState,
): {
dom: HTMLElement;
contentDOM?: HTMLElement;
ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
destroy?: () => void;
} => {
if ("isToggleable" in block.props && !block.props.isToggleable) {
return {
dom: renderedElement,
};
}
const dom = document.createElement("div");
const toggleWrapper = document.createElement("div");
toggleWrapper.className = "bn-toggle-wrapper";
const toggleButton = document.createElement("button");
toggleButton.className = "bn-toggle-button";
toggleButton.type = "button";
toggleButton.innerHTML =
// https://fonts.google.com/icons?selected=Material+Symbols+Rounded:chevron_right:FILL@0;wght@700;GRAD@0;opsz@24&icon.query=chevron&icon.style=Rounded&icon.size=24&icon.color=%23e8eaed
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="CURRENTCOLOR"><path d="M320-200v-560l440 280-440 280Z"/></svg>';
const toggleButtonMouseDown = (event: MouseEvent) => event.preventDefault();
toggleButton.addEventListener("mousedown", toggleButtonMouseDown);
const toggleButtonOnClick = () => {
// Toggles visibility of child blocks. Also adds/removes the "add block"
// button if there are no child blocks.
if (toggleWrapper.getAttribute("data-show-children") === "true") {
toggleWrapper.setAttribute("data-show-children", "false");
toggledState.set(editor.getBlock(block)!, false);
if (dom.contains(toggleAddBlockButton)) {
dom.removeChild(toggleAddBlockButton);
}
} else {
toggleWrapper.setAttribute("data-show-children", "true");
toggledState.set(editor.getBlock(block)!, true);
if (
editor.getBlock(block)?.children.length === 0 &&
!dom.contains(toggleAddBlockButton)
) {
dom.appendChild(toggleAddBlockButton);
}
}
};
toggleButton.addEventListener("click", toggleButtonOnClick);
toggleWrapper.appendChild(toggleButton);
toggleWrapper.appendChild(renderedElement);
const toggleAddBlockButton = document.createElement("button");
toggleAddBlockButton.className = "bn-toggle-add-block-button";
toggleAddBlockButton.type = "button";
toggleAddBlockButton.textContent =
editor.dictionary.toggle_blocks.add_block_button;
const toggleAddBlockButtonMouseDown = (event: MouseEvent) =>
event.preventDefault();
toggleAddBlockButton.addEventListener(
"mousedown",
toggleAddBlockButtonMouseDown,
);
const toggleAddBlockButtonOnClick = () => {
// Adds a single empty child block.
editor.transact(() => {
// dom.removeChild(toggleAddBlockButton);
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);
let childCount = block.children.length;
const onEditorChange = editor.onChange(() => {
const newChildCount = editor.getBlock(block)?.children.length ?? 0;
if (newChildCount > childCount) {
// If a child block is added while children are hidden, show children.
if (toggleWrapper.getAttribute("data-show-children") === "false") {
toggleWrapper.setAttribute("data-show-children", "true");
toggledState.set(editor.getBlock(block)!, true);
}
// Remove the "add block" button as we want to show child blocks and
// there is at least one child block.
if (dom.contains(toggleAddBlockButton)) {
dom.removeChild(toggleAddBlockButton);
}
} else if (newChildCount === 0 && newChildCount < childCount) {
// If the last child block is removed while children are shown, hide
// children.
if (toggleWrapper.getAttribute("data-show-children") === "true") {
toggleWrapper.setAttribute("data-show-children", "false");
toggledState.set(editor.getBlock(block)!, false);
}
// Remove the "add block" button as we want to hide child blocks,
// regardless of whether there are child blocks or not.
if (dom.contains(toggleAddBlockButton)) {
dom.removeChild(toggleAddBlockButton);
}
}
childCount = newChildCount;
});
if (toggledState.get(block)) {
toggleWrapper.setAttribute("data-show-children", "true");
if (block.children.length === 0) {
// If the toggle is set to show children, but there are no children,
// we add the "add block" button.
dom.appendChild(toggleAddBlockButton);
}
} else {
toggleWrapper.setAttribute("data-show-children", "false");
}
return {
dom,
// Prevents re-renders when the toggle button is clicked.
ignoreMutation: (mutation) => {
if (
mutation instanceof MutationRecord &&
// We want to prevent re-renders when the view changes, so we ignore
// all mutations where the `data-show-children` attribute is changed
// or the "add block" button is added/removed.
((mutation.type === "attributes" &&
mutation.target === toggleWrapper &&
mutation.attributeName === "data-show-children") ||
(mutation.type === "childList" &&
(mutation.addedNodes[0] === toggleAddBlockButton ||
mutation.removedNodes[0] === toggleAddBlockButton)))
) {
return true;
}
return false;
},
destroy: () => {
toggleButton.removeEventListener("mousedown", toggleButtonMouseDown);
toggleButton.removeEventListener("click", toggleButtonOnClick);
toggleAddBlockButton.removeEventListener(
"mousedown",
toggleAddBlockButtonMouseDown,
);
toggleAddBlockButton.removeEventListener(
"click",
toggleAddBlockButtonOnClick,
);
onEditorChange?.();
},
};
};