Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
30018f5
simplify colorpicker
YousefED Mar 17, 2023
6e0fafa
Added basic mouse cursor position
matthewlipski Apr 1, 2023
6dfd444
Added drag handle menu customization API
matthewlipski Apr 1, 2023
77591d7
Copied changes from PR and minor improvements
matthewlipski Apr 3, 2023
50f698a
Merge branch 'custom-side-menu' into custom-sidemenu
matthewlipski Apr 3, 2023
7e024d8
Small changes
matthewlipski Apr 3, 2023
cd7c027
Added `DragHandleMenuItem` component
matthewlipski Apr 3, 2023
207a8d8
Fixed side menu unnecessary updates
matthewlipski Apr 4, 2023
ac6cc6d
Removed unnecessary state
matthewlipski Apr 4, 2023
6c3de6d
Cleaned up code
matthewlipski Apr 4, 2023
fca4c2a
Changed how the side menu gets the hovered block
matthewlipski Apr 12, 2023
d39536d
Added side menu image
matthewlipski Apr 13, 2023
de13ab2
Added docs
matthewlipski Apr 13, 2023
279b70c
Fixed comments in demo
matthewlipski Apr 13, 2023
ee61bd0
Made all menus/toolbars scroll with page
matthewlipski Apr 14, 2023
a8cb7d5
Revert "Made all menus/toolbars scroll with page"
matthewlipski Apr 14, 2023
653e0ce
Removed `editor` from dynamic props
matthewlipski Apr 19, 2023
f06770a
Documentation changes
matthewlipski Apr 19, 2023
0f44ebc
Small cleanup
matthewlipski Apr 19, 2023
df7fceb
Merge branch 'custom-formattingtoolbar' into custom-sidemenu
matthewlipski Apr 19, 2023
581c8cc
Vanilla example fix
matthewlipski Apr 19, 2023
28db09f
Merge remote-tracking branch 'origin/custom-sidemenu' into custom-sid…
matthewlipski Apr 19, 2023
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
1 change: 1 addition & 0 deletions packages/core/src/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const getBlockNoteExtensions = (opts: {
if (opts.uiFactories.blockSideMenuFactory) {
ret.push(
DraggableBlocksExtension.configure({
editor: opts.editor,
blockSideMenuFactory: opts.uiFactories.blockSideMenuFactory,
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { EditorElement, ElementFactory } from "../../shared/EditorElement";
import { BlockNoteEditor } from "../../BlockNoteEditor";
import { Block } from "../Blocks/api/blockTypes";

export type BlockSideMenuStaticParams = {
editor: BlockNoteEditor;

addBlock: () => void;
deleteBlock: () => void;

blockDragStart: (event: DragEvent) => void;
blockDragEnd: () => void;

freezeMenu: () => void;
unfreezeMenu: () => void;

setBlockTextColor: (color: string) => void;
setBlockBackgroundColor: (color: string) => void;
};

export type BlockSideMenuDynamicParams = {
blockTextColor: string;
blockBackgroundColor: string;
block: Block;

referenceRect: DOMRect;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Editor, Extension } from "@tiptap/core";
import { BlockSideMenuFactory } from "./BlockSideMenuFactoryTypes";
import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin";
import { BlockNoteEditor } from "../../BlockNoteEditor";

export type DraggableBlocksOptions = {
editor: Editor;
tiptapEditor: Editor;
editor: BlockNoteEditor;
blockSideMenuFactory: BlockSideMenuFactory;
};

Expand All @@ -25,7 +27,8 @@ export const DraggableBlocksExtension =
}
return [
createDraggableBlocksPlugin({
editor: this.editor,
tiptapEditor: this.editor,
editor: this.options.editor,
blockSideMenuFactory: this.options.blockSideMenuFactory,
}),
];
Expand Down
141 changes: 38 additions & 103 deletions packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,13 @@ import {
} from "./BlockSideMenuFactoryTypes";
import { DraggableBlocksOptions } from "./DraggableBlocksExtension";
import { MultipleNodeSelection } from "./MultipleNodeSelection";
import { BlockNoteEditor } from "../../BlockNoteEditor";

const serializeForClipboard = (pv as any).__serializeForClipboard;
// code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799

let dragImageElement: Element | undefined;

export function createRect(rect: DOMRect) {
let newRect = {
left: rect.left + document.body.scrollLeft,
top: rect.top + document.body.scrollTop,
width: rect.width,
height: rect.height,
bottom: 0,
right: 0,
};
newRect.bottom = newRect.top + newRect.height;
newRect.right = newRect.left + newRect.width;
return newRect;
}

function getDraggableBlockFromCoords(
coords: { left: number; top: number },
view: EditorView
Expand Down Expand Up @@ -237,13 +224,15 @@ function dragStart(e: DragEvent, view: EditorView) {
}

export type BlockMenuViewProps = {
editor: Editor;
tiptapEditor: Editor;
editor: BlockNoteEditor;
blockMenuFactory: BlockSideMenuFactory;
horizontalPosAnchoredAtRoot: boolean;
};

export class BlockMenuView {
editor: Editor;
editor: BlockNoteEditor;
private ttEditor: Editor;

// When true, the drag handle with be anchored at the same level as root elements
// When false, the drag handle with be just to the left of the element
Expand All @@ -253,20 +242,22 @@ export class BlockMenuView {

blockMenu: BlockSideMenu;

hoveredBlockContent: HTMLElement | undefined;
hoveredBlock: HTMLElement | undefined;

menuOpen = false;
menuFrozen = false;

constructor({
tiptapEditor,
editor,
blockMenuFactory,
horizontalPosAnchoredAtRoot,
}: BlockMenuViewProps) {
this.editor = editor;
this.ttEditor = tiptapEditor;
this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot;
this.horizontalPosAnchor = (
editor.view.dom.firstChild! as HTMLElement
this.ttEditor.view.dom.firstChild! as HTMLElement
).getBoundingClientRect().x;

this.blockMenu = blockMenuFactory(this.getStaticParams());
Expand All @@ -287,31 +278,31 @@ export class BlockMenuView {
}

/**
* If the event is outside of the editor contents,
* If the event is outside the editor contents,
* we dispatch a fake event, so that we can still drop the content
* when dragging / dropping to the side of the editor
*/
onDrop = (event: DragEvent) => {
if ((event as any).synthetic) {
return;
}
let pos = this.editor.view.posAtCoords({
let pos = this.ttEditor.view.posAtCoords({
left: event.clientX,
top: event.clientY,
});

if (!pos || pos.inside === -1) {
const evt = new Event("drop", event) as any;
const editorBoundingBox = (
this.editor.view.dom.firstChild! as HTMLElement
this.ttEditor.view.dom.firstChild! as HTMLElement
).getBoundingClientRect();
evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
evt.clientY = event.clientY;
evt.dataTransfer = event.dataTransfer;
evt.preventDefault = () => event.preventDefault();
evt.synthetic = true; // prevent recursion
// console.log("dispatch fake drop");
this.editor.view.dom.dispatchEvent(evt);
this.ttEditor.view.dom.dispatchEvent(evt);
}
};

Expand All @@ -324,23 +315,23 @@ export class BlockMenuView {
if ((event as any).synthetic) {
return;
}
let pos = this.editor.view.posAtCoords({
let pos = this.ttEditor.view.posAtCoords({
left: event.clientX,
top: event.clientY,
});

if (!pos || pos.inside === -1) {
const evt = new Event("dragover", event) as any;
const editorBoundingBox = (
this.editor.view.dom.firstChild! as HTMLElement
this.ttEditor.view.dom.firstChild! as HTMLElement
).getBoundingClientRect();
evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
evt.clientY = event.clientY;
evt.dataTransfer = event.dataTransfer;
evt.preventDefault = () => event.preventDefault();
evt.synthetic = true; // prevent recursion
// console.log("dispatch fake dragover");
this.editor.view.dom.dispatchEvent(evt);
this.ttEditor.view.dom.dispatchEvent(evt);
}
};

Expand Down Expand Up @@ -374,7 +365,7 @@ export class BlockMenuView {
// Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
// the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
const editorBoundingBox = (
this.editor.view.dom.firstChild! as HTMLElement
this.ttEditor.view.dom.firstChild! as HTMLElement
).getBoundingClientRect();

this.horizontalPosAnchor = editorBoundingBox.x;
Expand All @@ -384,7 +375,7 @@ export class BlockMenuView {
left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor
top: event.clientY,
};
const block = getDraggableBlockFromCoords(coords, this.editor.view);
const block = getDraggableBlockFromCoords(coords, this.ttEditor.view);

// Closes the menu if the mouse cursor is beyond the editor vertically.
if (!block || !this.editor.isEditable) {
Expand All @@ -399,15 +390,16 @@ export class BlockMenuView {
// Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
if (
this.menuOpen &&
this.hoveredBlockContent?.hasAttribute("data-id") &&
this.hoveredBlockContent?.getAttribute("data-id") === block.id
this.hoveredBlock?.hasAttribute("data-id") &&
this.hoveredBlock?.getAttribute("data-id") === block.id
) {
return;
}

this.hoveredBlock = block.node;

// Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
const blockContent = block.node.firstChild as HTMLElement;
this.hoveredBlockContent = blockContent;

if (!blockContent) {
return;
Expand Down Expand Up @@ -448,18 +440,18 @@ export class BlockMenuView {
this.menuFrozen = true;
this.blockMenu.hide();

const blockContentBoundingBox =
this.hoveredBlockContent!.getBoundingClientRect();
const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
const blockContentBoundingBox = blockContent.getBoundingClientRect();

const pos = this.editor.view.posAtCoords({
const pos = this.ttEditor.view.posAtCoords({
left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
});
if (!pos) {
return;
}

const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos);
const blockInfo = getBlockInfoFromPos(this.ttEditor.state.doc, pos.pos);
if (blockInfo === undefined) {
return;
}
Expand All @@ -471,107 +463,49 @@ export class BlockMenuView {
const newBlockInsertionPos = endPos + 1;
const newBlockContentPos = newBlockInsertionPos + 2;

this.editor
this.ttEditor
.chain()
.BNCreateBlock(newBlockInsertionPos)
.BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
.setTextSelection(newBlockContentPos)
.run();
} else {
this.editor.commands.setTextSelection(endPos);
this.ttEditor.commands.setTextSelection(endPos);
}

// Focuses and activates the suggestion menu.
this.editor.view.focus();
this.editor.view.dispatch(
this.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
this.ttEditor.view.focus();
this.ttEditor.view.dispatch(
this.ttEditor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
// TODO import suggestion plugin key
activate: true,
type: "drag",
})
);
}

deleteBlock() {
this.menuOpen = false;
this.blockMenu.hide();

const blockContentBoundingBox =
this.hoveredBlockContent!.getBoundingClientRect();

const pos = this.editor.view.posAtCoords({
left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
});
if (!pos) {
return;
}

this.editor.commands.BNDeleteBlock(pos.pos);
}

setBlockBackgroundColor(color: string) {
this.menuOpen = false;
this.blockMenu.hide();

const blockContentBoundingBox =
this.hoveredBlockContent!.getBoundingClientRect();

const pos = this.editor.view.posAtCoords({
left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
});
if (!pos) {
return;
}

this.editor.commands.setBlockBackgroundColor(pos.pos, color);
}

setBlockTextColor(color: string) {
this.menuOpen = false;
this.blockMenu.hide();

const blockContentBoundingBox =
this.hoveredBlockContent!.getBoundingClientRect();

const pos = this.editor.view.posAtCoords({
left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
});
if (!pos) {
return;
}

this.editor.commands.setBlockTextColor(pos.pos, color);
}

getStaticParams(): BlockSideMenuStaticParams {
return {
editor: this.editor,
addBlock: () => this.addBlock(),
deleteBlock: () => this.deleteBlock(),
blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view),
blockDragStart: (event: DragEvent) =>
dragStart(event, this.ttEditor.view),
blockDragEnd: () => unsetDragImage(),
freezeMenu: () => {
this.menuFrozen = true;
},
unfreezeMenu: () => {
this.menuFrozen = false;
},
setBlockBackgroundColor: (color: string) =>
this.setBlockBackgroundColor(color),
setBlockTextColor: (color: string) => this.setBlockTextColor(color),
};
}

getDynamicParams(): BlockSideMenuDynamicParams {
const blockContentBoundingBox =
this.hoveredBlockContent!.getBoundingClientRect();
const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
const blockContentBoundingBox = blockContent.getBoundingClientRect();

return {
blockBackgroundColor:
this.editor.getAttributes("blockContainer").backgroundColor,
blockTextColor: this.editor.getAttributes("blockContainer").textColor,
block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!,
referenceRect: new DOMRect(
this.horizontalPosAnchoredAtRoot
? this.horizontalPosAnchor
Expand All @@ -591,6 +525,7 @@ export const createDraggableBlocksPlugin = (
key: new PluginKey("DraggableBlocksPlugin"),
view: () =>
new BlockMenuView({
tiptapEditor: options.tiptapEditor,
editor: options.editor,
blockMenuFactory: options.blockSideMenuFactory,
horizontalPosAnchoredAtRoot: true,
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@emotion/react": "^11.10.5",
"@mantine/core": "^5.6.1",
"@mantine/hooks": "^5.6.1",
"@mantine/utils": "^6.0.5",
"@tippyjs/react": "^4.2.6",
"@tiptap/react": "2.0.0-beta.217",
"react-icons": "^4.3.1"
Expand Down
Loading