From e62e69dbf6b6d0c4b8c5f22af871eebba48787cd Mon Sep 17 00:00:00 2001 From: Matthew Lipski <50169049+matthewlipski@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:17:27 +0200 Subject: [PATCH] feat: Non-selectable custom blocks (#1090) * simple fix for text selection in non selectable nodes * Added `isSelectable` field to `blockConfig` * Added comments * Added cut event handling --------- Co-authored-by: yousefed --- packages/core/src/schema/blocks/createSpec.ts | 33 +++++++++++++++++-- packages/core/src/schema/blocks/types.ts | 2 ++ packages/react/src/schema/ReactBlockSpec.tsx | 17 +++++++--- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 25fb94a9a..3689a108c 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -1,4 +1,6 @@ +import { Editor } from "@tiptap/core"; import { TagParseRule } from "@tiptap/pm/model"; +import { NodeView } from "@tiptap/pm/view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { InlineContentSchema } from "../inlineContent/types"; import { StyleSchema } from "../styles/types"; @@ -61,6 +63,27 @@ export type CustomBlockImplementation< ) => PartialBlockFromConfig["props"] | undefined; }; +// Function that enables copying of selected content within non-selectable +// blocks. +export function applyNonSelectableBlockFix(nodeView: NodeView, editor: Editor) { + nodeView.stopEvent = (event) => { + // Ensures copy events are handled by the browser and not by ProseMirror. + if (event.type === "copy" || event.type === "cut") { + return true; + } + // Blurs the editor on mouse down as the block is non-selectable. This is + // mainly done to prevent UI elements like the formatting toolbar from being + // visible while content within a non-selectable block is selected. + if (event.type === "mousedown") { + setTimeout(() => { + editor.view.dom.blur(); + }, 10); + return true; + } + return false; + }; +} + // Function that uses the 'parse' function of a blockConfig to create a // TipTap node's `parseHTML` property. This is only used for parsing content // from the clipboard. @@ -125,7 +148,7 @@ export function createBlockSpec< ? "inline*" : "") as T["content"] extends "inline" ? "inline*" : "", group: "blockContent", - selectable: true, + selectable: blockConfig.isSelectable ?? true, addAttributes() { return propsToAttributes(blockConfig.propSchema); @@ -163,13 +186,19 @@ export function createBlockSpec< const output = blockImplementation.render(block as any, editor); - return wrapInBlockStructure( + const nodeView: NodeView = wrapInBlockStructure( output, block.type, block.props, blockConfig.propSchema, blockContentDOMAttributes ); + + if (blockConfig.isSelectable === false) { + applyNonSelectableBlockFix(nodeView, this.editor); + } + + return nodeView; }; }, }); diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 1caf78db8..e5712d08b 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -49,6 +49,7 @@ export type FileBlockConfig = { }; }; content: "none"; + isSelectable?: boolean; isFileBlock: true; fileBlockAccept?: string[]; }; @@ -60,6 +61,7 @@ export type BlockConfig = type: string; readonly propSchema: PropSchema; content: "inline" | "none" | "table"; + isSelectable?: boolean; isFileBlock?: false; } | FileBlockConfig; diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index ba36009a6..687f93afe 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -1,4 +1,5 @@ import { + applyNonSelectableBlockFix, BlockFromConfig, BlockNoteEditor, BlockSchemaWithBlock, @@ -18,6 +19,7 @@ import { StyleSchema, } from "@blocknote/core"; import { + NodeView, NodeViewContent, NodeViewProps, NodeViewWrapper, @@ -118,7 +120,7 @@ export function createReactBlockSpec< ? "inline*" : "") as T["content"] extends "inline" ? "inline*" : "", group: "blockContent", - selectable: true, + selectable: blockConfig.isSelectable ?? true, addAttributes() { return propsToAttributes(blockConfig.propSchema); @@ -140,8 +142,8 @@ export function createReactBlockSpec< }, addNodeView() { - return (props) => - ReactNodeViewRenderer( + return (props) => { + const nodeView = ReactNodeViewRenderer( (props: NodeViewProps) => { // Gets the BlockNote editor instance const editor = this.options.editor! as BlockNoteEditor; @@ -178,7 +180,14 @@ export function createReactBlockSpec< { className: "bn-react-node-view-renderer", } - )(props); + )(props) as NodeView; + + if (blockConfig.isSelectable === false) { + applyNonSelectableBlockFix(nodeView, this.editor); + } + + return nodeView; + }; }, });