diff --git a/examples/vanilla/src/ui/formattingToolbarFactory.ts b/examples/vanilla/src/ui/formattingToolbarFactory.ts index 6dffb819ab..e35a58820f 100644 --- a/examples/vanilla/src/ui/formattingToolbarFactory.ts +++ b/examples/vanilla/src/ui/formattingToolbarFactory.ts @@ -14,12 +14,12 @@ export const formattingToolbarFactory: FormattingToolbarFactory = ( container.style.padding = "10px"; container.style.opacity = "0.8"; const boldBtn = createButton("set bold", () => { - staticParams.toggleBold(); + staticParams.editor.toggleStyles({ bold: true }); }); container.appendChild(boldBtn); const linkBtn = createButton("set link", () => { - staticParams.setHyperlink("https://www.google.com"); + staticParams.editor.createLink("https://www.google.com"); }); container.appendChild(boldBtn); @@ -34,7 +34,8 @@ export const formattingToolbarFactory: FormattingToolbarFactory = ( container.style.display = "block"; } - boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; + boldBtn.text = + "bold" in staticParams.editor.getActiveStyles() ? "unset bold" : "set bold"; container.style.top = params.referenceRect.y + "px"; container.style.left = params.referenceRect.x + "px"; }, diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index ae832f488a..beea32c5f7 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -23,7 +23,13 @@ import { BlockIdentifier, PartialBlock, } from "./extensions/Blocks/api/blockTypes"; +import { + ColorStyle, + Styles, + ToggledStyle, +} from "./extensions/Blocks/api/inlineContentTypes"; import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; +import { Selection } from "./extensions/Blocks/api/selectionTypes"; import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos"; import { BaseSlashMenuItem, @@ -102,6 +108,10 @@ export class BlockNoteEditor { return this._tiptapEditor.view.dom as HTMLDivElement; } + public focus() { + this._tiptapEditor.view.focus(); + } + constructor(options: Partial = {}) { // apply defaults options = { @@ -229,7 +239,7 @@ export class BlockNoteEditor { function traverseBlockArray(blockArray: Block[]): boolean { for (const block of blockArray) { - if (callback(block) === false) { + if (!callback(block)) { return false; } @@ -237,7 +247,7 @@ export class BlockNoteEditor { ? block.children.slice().reverse() : block.children; - if (traverseBlockArray(children) === false) { + if (!traverseBlockArray(children)) { return false; } } @@ -319,6 +329,44 @@ export class BlockNoteEditor { } } + /** + * Gets a snapshot of the current selection. + */ + public getSelection(): Selection | undefined { + if ( + this._tiptapEditor.state.selection.from === + this._tiptapEditor.state.selection.to + ) { + return undefined; + } + + const blocks: Block[] = []; + + this._tiptapEditor.state.doc.descendants((node, pos) => { + if (node.type.spec.group !== "blockContent") { + return true; + } + + if ( + pos + node.nodeSize < this._tiptapEditor.state.selection.from || + pos > this._tiptapEditor.state.selection.to + ) { + return true; + } + + blocks.push( + nodeToBlock( + this._tiptapEditor.state.doc.resolve(pos).node(), + this.blockCache + ) + ); + + return false; + }); + + return { blocks: blocks }; + } + /** * Checks if the editor is currently editable, or if it's locked. * @returns True if the editor is editable, false otherwise. @@ -384,6 +432,169 @@ export class BlockNoteEditor { replaceBlocks(blocksToRemove, blocksToInsert, this._tiptapEditor); } + /** + * Gets the active text styles at the text cursor position or at the end of the current selection if it's active. + */ + public getActiveStyles() { + const styles: Styles = {}; + const marks = this._tiptapEditor.state.selection.$to.marks(); + + const toggleStyles = new Set([ + "bold", + "italic", + "underline", + "strike", + "code", + ]); + const colorStyles = new Set(["textColor", "backgroundColor"]); + + for (const mark of marks) { + if (toggleStyles.has(mark.type.name as ToggledStyle)) { + styles[mark.type.name as ToggledStyle] = true; + } else if (colorStyles.has(mark.type.name as ColorStyle)) { + styles[mark.type.name as ColorStyle] = mark.attrs.color; + } + } + + return styles; + } + + /** + * Adds styles to the currently selected content. + * @param styles The styles to add. + */ + public addStyles(styles: Styles) { + const toggleStyles = new Set([ + "bold", + "italic", + "underline", + "strike", + "code", + ]); + const colorStyles = new Set(["textColor", "backgroundColor"]); + + for (const [style, value] of Object.entries(styles)) { + if (toggleStyles.has(style as ToggledStyle)) { + this._tiptapEditor.commands.setMark(style); + } else if (colorStyles.has(style as ColorStyle)) { + this._tiptapEditor.commands.setMark(style, { color: value }); + } + } + } + + /** + * Removes styles from the currently selected content. + * @param styles The styles to remove. + */ + public removeStyles(styles: Styles) { + for (const style of Object.keys(styles)) { + this._tiptapEditor.commands.unsetMark(style); + } + } + + /** + * Toggles styles on the currently selected content. + * @param styles The styles to toggle. + */ + public toggleStyles(styles: Styles) { + const toggleStyles = new Set([ + "bold", + "italic", + "underline", + "strike", + "code", + ]); + const colorStyles = new Set(["textColor", "backgroundColor"]); + + for (const [style, value] of Object.entries(styles)) { + if (toggleStyles.has(style as ToggledStyle)) { + this._tiptapEditor.commands.toggleMark(style); + } else if (colorStyles.has(style as ColorStyle)) { + this._tiptapEditor.commands.toggleMark(style, { color: value }); + } + } + } + + /** + * Gets the currently selected text. + */ + public getSelectedText() { + return this._tiptapEditor.state.doc.textBetween( + this._tiptapEditor.state.selection.from, + this._tiptapEditor.state.selection.to + ); + } + + /** + * Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection. + */ + public getSelectedLinkUrl() { + return this._tiptapEditor.getAttributes("link").href as string | undefined; + } + + /** + * Creates a new link to replace the selected content. + * @param url The link URL. + * @param text The text to display the link with. + */ + public createLink(url: string, text?: string) { + if (url === "") { + return; + } + + let { from, to } = this._tiptapEditor.state.selection; + + if (!text) { + text = this._tiptapEditor.state.doc.textBetween(from, to); + } + + const mark = this._tiptapEditor.schema.mark("link", { href: url }); + + this._tiptapEditor.view.dispatch( + this._tiptapEditor.view.state.tr + .insertText(text, from, to) + .addMark(from, from + text.length, mark) + ); + } + + /** + * Checks if the block containing the text cursor can be nested. + */ + public canNestBlock() { + const { startPos, depth } = getBlockInfoFromPos( + this._tiptapEditor.state.doc, + this._tiptapEditor.state.selection.from + )!; + + return this._tiptapEditor.state.doc.resolve(startPos).index(depth - 1) > 0; + } + + /** + * Nests the block containing the text cursor into the block above it. + */ + public nestBlock() { + this._tiptapEditor.commands.sinkListItem("blockContainer"); + } + + /** + * Checks if the block containing the text cursor is nested. + */ + public canUnnestBlock() { + const { depth } = getBlockInfoFromPos( + this._tiptapEditor.state.doc, + this._tiptapEditor.state.selection.from + )!; + + return depth > 2; + } + + /** + * Lifts the block containing the text cursor out of its parent. + */ + public unnestBlock() { + this._tiptapEditor.commands.liftListItem("blockContainer"); + } + /** * Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list * items are un-nested in the output HTML. diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index bb47891ba0..c0a0e3aedf 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -24,8 +24,7 @@ import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/Formatt import HyperlinkMark from "./extensions/HyperlinkToolbar/HyperlinkMark"; import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; -import { SlashMenuExtension } from "./extensions/SlashMenu"; -import { BaseSlashMenuItem } from "./extensions/SlashMenu"; +import { BaseSlashMenuItem, SlashMenuExtension } from "./extensions/SlashMenu"; import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension"; import { TextColorExtension } from "./extensions/TextColor/TextColorExtension"; import { TextColorMark } from "./extensions/TextColor/TextColorMark"; @@ -100,6 +99,7 @@ export const getBlockNoteExtensions = (opts: { if (opts.uiFactories.blockSideMenuFactory) { ret.push( DraggableBlocksExtension.configure({ + editor: opts.editor, blockSideMenuFactory: opts.uiFactories.blockSideMenuFactory, }) ); @@ -108,6 +108,7 @@ export const getBlockNoteExtensions = (opts: { if (opts.uiFactories.formattingToolbarFactory) { ret.push( FormattingToolbarExtension.configure({ + editor: opts.editor, formattingToolbarFactory: opts.uiFactories.formattingToolbarFactory, }) ); diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index d76023338c..9a71910367 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -6,27 +6,27 @@ import { PartialBlock, } from "../../extensions/Blocks/api/blockTypes"; import { - ColorStyles, + ColorStyle, InlineContent, Link, PartialInlineContent, PartialLink, StyledText, Styles, - ToggledStyles, + ToggledStyle, } from "../../extensions/Blocks/api/inlineContentTypes"; import { getBlockInfoFromPos } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { UnreachableCaseError } from "../../shared/utils"; -const toggleStyles = new Set([ +const toggleStyles = new Set([ "bold", "italic", "underline", "strike", "code", ]); -const colorStyles = new Set(["textColor", "backgroundColor"]); +const colorStyles = new Set(["textColor", "backgroundColor"]); /** * Convert a StyledText inline element to a @@ -36,9 +36,9 @@ function styledTextToNode(styledText: StyledText, schema: Schema): Node { const marks: Mark[] = []; for (const [style, value] of Object.entries(styledText.styles)) { - if (toggleStyles.has(style as ToggledStyles)) { + if (toggleStyles.has(style as ToggledStyle)) { marks.push(schema.mark(style)); - } else if (colorStyles.has(style as ColorStyles)) { + } else if (colorStyles.has(style as ColorStyle)) { marks.push(schema.mark(style, { color: value })); } } @@ -168,10 +168,10 @@ function contentNodeToInlineContent(contentNode: Node) { for (const mark of node.marks) { if (mark.type.name === "link") { linkMark = mark; - } else if (toggleStyles.has(mark.type.name as ToggledStyles)) { - styles[mark.type.name as ToggledStyles] = true; - } else if (colorStyles.has(mark.type.name as ColorStyles)) { - styles[mark.type.name as ColorStyles] = mark.attrs.color; + } else if (toggleStyles.has(mark.type.name as ToggledStyle)) { + styles[mark.type.name as ToggledStyle] = true; + } else if (colorStyles.has(mark.type.name as ColorStyle)) { + styles[mark.type.name as ColorStyle] = mark.attrs.color; } else { throw Error("Mark is of an unrecognized type: " + mark.type.name); } diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts index 7284b5cce4..9d63930d95 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts @@ -8,11 +8,11 @@ export type Styles = { backgroundColor?: string; }; -export type ToggledStyles = { +export type ToggledStyle = { [K in keyof Styles]-?: Required[K] extends true ? K : never; }[keyof Styles]; -export type ColorStyles = { +export type ColorStyle = { [K in keyof Styles]-?: Required[K] extends string ? K : never; }[keyof Styles]; diff --git a/packages/core/src/extensions/Blocks/api/selectionTypes.ts b/packages/core/src/extensions/Blocks/api/selectionTypes.ts new file mode 100644 index 0000000000..9fbd40d6b6 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/selectionTypes.ts @@ -0,0 +1,5 @@ +import { Block } from "./blockTypes"; + +export type Selection = { + blocks: Block[]; +}; diff --git a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts index fed0df3068..b376feacc5 100644 --- a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +++ b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts @@ -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; }; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index 8f4a7af603..9156368eed 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -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; }; @@ -25,7 +27,8 @@ export const DraggableBlocksExtension = } return [ createDraggableBlocksPlugin({ - editor: this.editor, + tiptapEditor: this.editor, + editor: this.options.editor, blockSideMenuFactory: this.options.blockSideMenuFactory, }), ]; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 0e1fd655a8..9cb42464ec 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -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 @@ -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 @@ -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()); @@ -287,7 +278,7 @@ 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 */ @@ -295,7 +286,7 @@ 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, }); @@ -303,7 +294,7 @@ export class BlockMenuView { 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; @@ -311,7 +302,7 @@ export class BlockMenuView { 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); } }; @@ -324,7 +315,7 @@ 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, }); @@ -332,7 +323,7 @@ export class BlockMenuView { 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; @@ -340,7 +331,7 @@ export class BlockMenuView { 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); } }; @@ -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; @@ -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) { @@ -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; @@ -448,10 +440,10 @@ 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, }); @@ -459,7 +451,7 @@ export class BlockMenuView { return; } - const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos); + const blockInfo = getBlockInfoFromPos(this.ttEditor.state.doc, pos.pos); if (blockInfo === undefined) { return; } @@ -471,20 +463,20 @@ 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", @@ -492,65 +484,12 @@ export class BlockMenuView { ); } - 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; @@ -558,20 +497,15 @@ export class BlockMenuView { 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 @@ -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, diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts index fb63801145..283f92160e 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts @@ -1,5 +1,6 @@ import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; +import { BlockNoteEditor } from "../.."; import { FormattingToolbarFactory } from "./FormattingToolbarFactoryTypes"; import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin"; @@ -8,19 +9,21 @@ import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin"; */ export const FormattingToolbarExtension = Extension.create<{ formattingToolbarFactory: FormattingToolbarFactory; + editor: BlockNoteEditor; }>({ name: "FormattingToolbarExtension", addProseMirrorPlugins() { - if (!this.options.formattingToolbarFactory) { + if (!this.options.formattingToolbarFactory || !this.options.editor) { throw new Error( - "UI Element factory not defined for FormattingToolbarExtension" + "required args not defined for FormattingToolbarExtension" ); } return [ createFormattingToolbarPlugin({ - editor: this.editor, + tiptapEditor: this.editor, + editor: this.options.editor, formattingToolbarFactory: this.options.formattingToolbarFactory, pluginKey: new PluginKey("FormattingToolbarPlugin"), }), diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts index 1dd4f740b7..6b1c506cbb 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts @@ -1,43 +1,11 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -import { Block, PartialBlock } from "../Blocks/api/blockTypes"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; export type FormattingToolbarStaticParams = { - toggleBold: () => void; - toggleItalic: () => void; - toggleUnderline: () => void; - toggleStrike: () => void; - setHyperlink: (url: string, text?: string) => void; - - setTextColor: (color: string) => void; - setBackgroundColor: (color: string) => void; - setTextAlignment: ( - textAlignment: "left" | "center" | "right" | "justify" - ) => void; - - increaseBlockIndent: () => void; - decreaseBlockIndent: () => void; - - updateBlock: (updatedBlock: PartialBlock) => void; + editor: BlockNoteEditor; }; export type FormattingToolbarDynamicParams = { - boldIsActive: boolean; - italicIsActive: boolean; - underlineIsActive: boolean; - strikeIsActive: boolean; - hyperlinkIsActive: boolean; - activeHyperlinkUrl: string; - activeHyperlinkText: string; - - textColor: string; - backgroundColor: string; - textAlignment: "left" | "center" | "right" | "justify"; - - canIncreaseBlockIndent: boolean; - canDecreaseBlockIndent: boolean; - - block: Block; - referenceRect: DOMRect; }; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 18efb73f64..b220913957 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -6,8 +6,7 @@ import { } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Block, PartialBlock } from "../Blocks/api/blockTypes"; -import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; +import { BlockNoteEditor } from "../.."; import { FormattingToolbar, FormattingToolbarDynamicParams, @@ -19,11 +18,12 @@ import { // https://github.com/ueberdosis/tiptap/pull/2596/files export interface FormattingToolbarPluginProps { pluginKey: PluginKey; - editor: Editor; + tiptapEditor: Editor; + editor: BlockNoteEditor; formattingToolbarFactory: FormattingToolbarFactory; shouldShow?: | ((props: { - editor: Editor; + editor: BlockNoteEditor; view: EditorView; state: EditorState; oldState?: EditorState; @@ -38,7 +38,8 @@ export type FormattingToolbarViewProps = FormattingToolbarPluginProps & { }; export class FormattingToolbarView { - public editor: Editor; + public editor: BlockNoteEditor; + private ttEditor: Editor; public view: EditorView; @@ -68,11 +69,13 @@ export class FormattingToolbarView { constructor({ editor, + tiptapEditor, formattingToolbarFactory, view, shouldShow, }: FormattingToolbarViewProps) { this.editor = editor; + this.ttEditor = tiptapEditor; this.view = view; this.formattingToolbar = formattingToolbarFactory(this.getStaticParams()); @@ -85,10 +88,10 @@ export class FormattingToolbarView { this.view.dom.addEventListener("mouseup", this.viewMouseupHandler); this.view.dom.addEventListener("dragstart", this.dragstartHandler); + this.ttEditor.on("focus", this.focusHandler); + this.ttEditor.on("blur", this.blurHandler); + document.addEventListener("scroll", this.scrollHandler); - - this.editor.on("focus", this.focusHandler); - this.editor.on("blur", this.blurHandler); } viewMousedownHandler = () => { @@ -97,7 +100,7 @@ export class FormattingToolbarView { viewMouseupHandler = () => { this.preventShow = false; - setTimeout(() => this.update(this.editor.view)); + setTimeout(() => this.update(this.ttEditor.view)); }; dragstartHandler = () => { @@ -107,7 +110,7 @@ export class FormattingToolbarView { focusHandler = () => { // we use `setTimeout` to make sure `selection` is already updated - setTimeout(() => this.update(this.editor.view)); + setTimeout(() => this.update(this.ttEditor.view)); }; blurHandler = ({ event }: { event: FocusEvent }) => { @@ -221,14 +224,14 @@ export class FormattingToolbarView { this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler); this.view.dom.removeEventListener("dragstart", this.dragstartHandler); - document.removeEventListener("scroll", this.scrollHandler); + this.ttEditor.off("focus", this.focusHandler); + this.ttEditor.off("blur", this.blurHandler); - this.editor.off("focus", this.focusHandler); - this.editor.off("blur", this.blurHandler); + document.removeEventListener("scroll", this.scrollHandler); } getSelectionBoundingBox() { - const { state } = this.editor.view; + const { state } = this.ttEditor.view; const { selection } = state; // support for CellSelections @@ -237,120 +240,24 @@ export class FormattingToolbarView { const to = Math.max(...ranges.map((range) => range.$to.pos)); if (isNodeSelection(selection)) { - const node = this.editor.view.nodeDOM(from) as HTMLElement; + const node = this.ttEditor.view.nodeDOM(from) as HTMLElement; if (node) { return node.getBoundingClientRect(); } } - return posToDOMRect(this.editor.view, from, to); + return posToDOMRect(this.ttEditor.view, from, to); } getStaticParams(): FormattingToolbarStaticParams { return { - toggleBold: () => { - this.editor.view.focus(); - this.editor.commands.toggleBold(); - }, - toggleItalic: () => { - this.editor.view.focus(); - this.editor.commands.toggleItalic(); - }, - toggleUnderline: () => { - this.editor.view.focus(); - this.editor.commands.toggleUnderline(); - }, - toggleStrike: () => { - this.editor.view.focus(); - this.editor.commands.toggleStrike(); - }, - setHyperlink: (url: string, text?: string) => { - if (url === "") { - return; - } - - let { from, to } = this.editor.state.selection; - - if (!text) { - text = this.editor.state.doc.textBetween(from, to); - } - - const mark = this.editor.schema.mark("link", { href: url }); - - this.editor.view.dispatch( - this.editor.view.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - ); - this.editor.view.focus(); - }, - setTextColor: (color: string) => { - this.editor.view.focus(); - this.editor.commands.setTextColor(color); - }, - setBackgroundColor: (color: string) => { - this.editor.view.focus(); - this.editor.commands.setBackgroundColor(color); - }, - setTextAlignment: ( - textAlignment: "left" | "center" | "right" | "justify" - ) => { - this.editor.view.focus(); - this.editor.commands.setTextAlignment(textAlignment); - }, - increaseBlockIndent: () => { - this.editor.view.focus(); - this.editor.commands.sinkListItem("blockContainer"); - }, - decreaseBlockIndent: () => { - this.editor.view.focus(); - this.editor.commands.liftListItem("blockContainer"); - }, - updateBlock: (updatedBlock: PartialBlock) => { - this.editor.view.focus(); - this.editor.commands.BNUpdateBlock( - this.editor.state.selection.from, - updatedBlock - ); - }, + editor: this.editor, }; } getDynamicParams(): FormattingToolbarDynamicParams { - const blockInfo = getBlockInfoFromPos( - this.editor.state.doc, - this.editor.state.selection.from - )!; - return { - boldIsActive: this.editor.isActive("bold"), - italicIsActive: this.editor.isActive("italic"), - underlineIsActive: this.editor.isActive("underline"), - strikeIsActive: this.editor.isActive("strike"), - hyperlinkIsActive: this.editor.isActive("link"), - activeHyperlinkUrl: this.editor.getAttributes("link").href || "", - activeHyperlinkText: this.editor.state.doc.textBetween( - this.editor.state.selection.from, - this.editor.state.selection.to - ), - textColor: this.editor.getAttributes("textColor").color || "default", - backgroundColor: - this.editor.getAttributes("backgroundColor").color || "default", - textAlignment: - this.editor.getAttributes(blockInfo.contentType).textAlignment || - "left", - canIncreaseBlockIndent: - this.editor.state.doc - .resolve(blockInfo.startPos) - .index(blockInfo.depth - 1) > 0, - canDecreaseBlockIndent: blockInfo.depth > 2, - // Needs type cast as there is no way to create a type that dynamically updates based on which extensions are - // loaded by the editor. - block: { - type: blockInfo.contentType.name, - props: blockInfo.contentNode.attrs, - } as Block, referenceRect: this.getSelectionBoundingBox(), }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 331ab75284..feaa83bcbd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,3 +9,4 @@ export * from "./extensions/SlashMenu/BaseSlashMenuItem"; export * from "./shared/EditorElement"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; +export * from "./extensions/Blocks/api/inlineContentTypes"; diff --git a/packages/react/package.json b/packages/react/package.json index caef8d5c83..1b73655115 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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" diff --git a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx index 94cfdb179d..fedc81ff58 100644 --- a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx +++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx @@ -1,21 +1,29 @@ +import { FC } from "react"; import { - BlockSideMenu, BlockSideMenuDynamicParams, - BlockSideMenuFactory, BlockSideMenuStaticParams, } from "@blocknote/core"; import { BlockSideMenu as ReactBlockSideMenu } from "./components/BlockSideMenu"; import { ReactElementFactory } from "../ElementFactory/components/ReactElementFactory"; +import { DragHandleMenuProps } from "./components/DragHandleMenu"; +import { DefaultDragHandleMenu } from "./components/DefaultDragHandleMenu"; -export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( - staticParams -): BlockSideMenu => - ReactElementFactory( - staticParams, - ReactBlockSideMenu, - { - animation: "fade", - offset: [0, 0], - placement: "left", - } - ); +export const createReactBlockSideMenuFactory = ( + dragHandleMenu: FC = DefaultDragHandleMenu +) => { + const CustomDragHandleMenu = dragHandleMenu; + const CustomBlockSideMenu = ( + props: BlockSideMenuStaticParams & BlockSideMenuDynamicParams + ) => ; + + return (staticParams: BlockSideMenuStaticParams) => + ReactElementFactory( + staticParams, + CustomBlockSideMenu, + { + animation: "fade", + offset: [0, 0], + placement: "left", + } + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx index ecdf2fef69..f828e5f696 100644 --- a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx +++ b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx @@ -1,27 +1,22 @@ +import { FC, useEffect, useRef, useState } from "react"; +import { ActionIcon, Group, Menu } from "@mantine/core"; +import { Block, BlockNoteEditor } from "@blocknote/core"; import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; -import { ActionIcon, createStyles, Group, Menu } from "@mantine/core"; -import { useEffect, useRef, useState } from "react"; -import { ColorPickerMenu } from "./ColorPickerMenu"; +import { DragHandleMenuProps } from "./DragHandleMenu"; +import { DefaultDragHandleMenu } from "./DefaultDragHandleMenu"; export type BlockSideMenuProps = { + editor: BlockNoteEditor; + block: Block; + dragHandleMenu?: FC; addBlock: () => void; - deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; blockDragEnd: () => void; freezeMenu: () => void; unfreezeMenu: () => void; - - blockBackgroundColor: string; - setBlockBackgroundColor: (color: string) => void; - blockTextColor: string; - setBlockTextColor: (color: string) => void; }; export const BlockSideMenu = (props: BlockSideMenuProps) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", - }); - const [dragHandleMenuOpened, setDragHandleMenuOpened] = useState(false); const dragHandleRef = useRef(null); @@ -42,6 +37,13 @@ export const BlockSideMenu = (props: BlockSideMenuProps) => { return; }, [props.blockDragEnd, props.blockDragStart]); + const closeMenu = () => { + setDragHandleMenuOpened(false); + props.unfreezeMenu(); + }; + + const DragHandleMenu = props.dragHandleMenu || DefaultDragHandleMenu; + return ( @@ -69,24 +71,11 @@ export const BlockSideMenu = (props: BlockSideMenuProps) => { - - { - setDragHandleMenuOpened(false); - props.unfreezeMenu(); - props.deleteBlock(); - }}> - Delete - - { - setDragHandleMenuOpened(false); - props.unfreezeMenu(); - }} - {...props} - /> - + ); diff --git a/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx b/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx deleted file mode 100644 index d936b86545..0000000000 --- a/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box, Menu } from "@mantine/core"; -import { HiChevronRight } from "react-icons/hi"; -import { ColorPicker } from "../../SharedComponents/ColorPicker/components/ColorPicker"; -import { useCallback, useState } from "react"; - -export const ColorPickerMenu = (props: { - onClick?: () => void; - - blockBackgroundColor: string; - setBlockBackgroundColor: (color: string) => void; - blockTextColor: string; - setBlockTextColor: (color: string) => void; -}) => { - const [opened, setOpened] = useState(false); - const [menuCloseTimer, setMenuCloseTimer] = useState< - NodeJS.Timeout | undefined - >(undefined); - const [buttonBackground, setButtonBackground] = useState( - undefined - ); - - const startMenuCloseTimer = useCallback(() => { - setMenuCloseTimer( - setTimeout(() => { - setOpened(false); - }, 250) - ); - }, []); - - const stopMenuCloseTimer = useCallback(() => { - if (menuCloseTimer) { - clearTimeout(menuCloseTimer); - setMenuCloseTimer(undefined); - } - setOpened(true); - }, [menuCloseTimer]); - - return ( - - { - startMenuCloseTimer(); - setButtonBackground(undefined); - }} - onMouseOver={() => { - stopMenuCloseTimer(); - setButtonBackground("#f1f3f5"); - }} - rightSection={ - - - - }> - Colors - - { - startMenuCloseTimer(); - setButtonBackground(undefined); - }} - onMouseOver={() => { - stopMenuCloseTimer(); - setButtonBackground("#f1f3f5"); - }} - style={{ marginLeft: "90px" }}> - - - - ); -}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx new file mode 100644 index 0000000000..ed748ab58c --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx @@ -0,0 +1,68 @@ +import { ReactNode, useCallback, useRef, useState } from "react"; +import { Box, Menu } from "@mantine/core"; +import { HiChevronRight } from "react-icons/hi"; +import { DragHandleMenuProps } from "../DragHandleMenu"; +import { DragHandleMenuItem } from "../DragHandleMenuItem"; +import { ColorPicker } from "../../../SharedComponents/ColorPicker/components/ColorPicker"; + +export const BlockColorsButton = ( + props: DragHandleMenuProps & { children: ReactNode } +) => { + const [opened, setOpened] = useState(false); + + const menuCloseTimer = useRef(); + + const startMenuCloseTimer = useCallback(() => { + if (menuCloseTimer.current) { + clearTimeout(menuCloseTimer.current); + } + menuCloseTimer.current = setTimeout(() => { + setOpened(false); + }, 250); + }, []); + + const stopMenuCloseTimer = useCallback(() => { + if (menuCloseTimer.current) { + clearTimeout(menuCloseTimer.current); + } + setOpened(true); + }, []); + + return ( + + + +
+
{props.children}
+ + + +
+
+ + + props.editor.updateBlock(props.block, { + props: { textColor: color }, + }) + } + setBackgroundColor={(color) => + props.editor.updateBlock(props.block, { + props: { backgroundColor: color }, + }) + } + /> + +
+
+ ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx new file mode 100644 index 0000000000..7111a8b71f --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from "react"; + +import { DragHandleMenuProps } from "../DragHandleMenu"; +import { DragHandleMenuItem } from "../DragHandleMenuItem"; + +export const RemoveBlockButton = ( + props: DragHandleMenuProps & { children: ReactNode } +) => { + return ( + props.editor.removeBlocks([props.block])}> + {props.children} + + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx b/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx new file mode 100644 index 0000000000..8f11a29afe --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx @@ -0,0 +1,10 @@ +import { DragHandleMenu, DragHandleMenuProps } from "./DragHandleMenu"; +import { RemoveBlockButton } from "./DefaultButtons/RemoveBlockButton"; +import { BlockColorsButton } from "./DefaultButtons/BlockColorsButton"; + +export const DefaultDragHandleMenu = (props: DragHandleMenuProps) => ( + + Delete + Colors + +); diff --git a/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx b/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx new file mode 100644 index 0000000000..6d7e227dfc --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { createStyles, Menu } from "@mantine/core"; +import { Block, BlockNoteEditor } from "@blocknote/core"; + +export type DragHandleMenuProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}; + +export const DragHandleMenu = (props: { children: ReactNode }) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "DragHandleMenu", + }); + + return ( + {props.children} + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx b/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx new file mode 100644 index 0000000000..b99774fb4a --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx @@ -0,0 +1,17 @@ +import { Menu } from "@mantine/core"; +import { PolymorphicComponentProps } from "@mantine/utils"; + +export type DragHandleMenuItemProps = PolymorphicComponentProps<"button"> & { + closeMenu: () => void; +}; + +export const DragHandleMenuItem = (props: DragHandleMenuItemProps) => ( + { + props.closeMenu(); + props.onClick?.(event); + }}> + {props.children} + +); diff --git a/packages/react/src/ElementFactory/components/EditorElementComponentWrapper.tsx b/packages/react/src/ElementFactory/components/EditorElementComponentWrapper.tsx index ee98bc9314..012870a8ea 100644 --- a/packages/react/src/ElementFactory/components/EditorElementComponentWrapper.tsx +++ b/packages/react/src/ElementFactory/components/EditorElementComponentWrapper.tsx @@ -2,7 +2,7 @@ import { MantineProvider } from "@mantine/core"; import Tippy, { TippyProps } from "@tippyjs/react"; import { RequiredDynamicParams } from "@blocknote/core"; import { BlockNoteTheme } from "../../BlockNoteTheme"; -import { useCallback, useState } from "react"; +import { FC, useCallback, useState } from "react"; /** * Component used in the ReactElementFactory to wrap the EditorElementComponent in a MantineProvider and Tippy @@ -21,9 +21,7 @@ export function EditorElementComponentWrapper< isOpen: boolean; staticParams: ElementStaticParams; dynamicParams: ElementDynamicParams; - editorElementComponent: ( - props: ElementStaticParams & ElementDynamicParams - ) => JSX.Element; + editorElementComponent: FC; tippyProps?: TippyProps; }) { const EditorElementComponent = props.editorElementComponent; diff --git a/packages/react/src/ElementFactory/components/ReactElementFactory.tsx b/packages/react/src/ElementFactory/components/ReactElementFactory.tsx index 53a651df81..2d60422863 100644 --- a/packages/react/src/ElementFactory/components/ReactElementFactory.tsx +++ b/packages/react/src/ElementFactory/components/ReactElementFactory.tsx @@ -1,3 +1,4 @@ +import { FC } from "react"; import { TippyProps } from "@tippyjs/react"; import { createRoot } from "react-dom/client"; import { EditorElement, RequiredDynamicParams } from "@blocknote/core"; @@ -20,9 +21,7 @@ export const ReactElementFactory = < ElementDynamicParams extends RequiredDynamicParams >( staticParams: ElementStaticParams, - EditorElementComponent: ( - props: ElementStaticParams & ElementDynamicParams - ) => JSX.Element, + EditorElementComponent: FC, tippyProps?: TippyProps ): EditorElement => { const rootElement = document.createElement("div"); diff --git a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx index e7bd4386c4..49d458a35d 100644 --- a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx +++ b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx @@ -1,19 +1,21 @@ import { - FormattingToolbar, - FormattingToolbarFactory, FormattingToolbarStaticParams, FormattingToolbarDynamicParams, + BlockNoteEditor, } from "@blocknote/core"; import { FormattingToolbar as ReactFormattingToolbar } from "./components/FormattingToolbar"; import { ReactElementFactory } from "../ElementFactory/components/ReactElementFactory"; +import { FC } from "react"; -export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( - staticParams -): FormattingToolbar => - ReactElementFactory< - FormattingToolbarStaticParams, - FormattingToolbarDynamicParams - >(staticParams, ReactFormattingToolbar, { - animation: "fade", - placement: "top-start", - }); +export const createReactFormattingToolbarFactory = ( + toolbar: FC<{ editor: BlockNoteEditor }> = ReactFormattingToolbar +) => { + return (staticParams: FormattingToolbarStaticParams) => + ReactElementFactory< + FormattingToolbarStaticParams, + FormattingToolbarDynamicParams + >(staticParams, toolbar, { + animation: "fade", + placement: "top-start", + }); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx new file mode 100644 index 0000000000..f6d6d490a7 --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import { Menu } from "@mantine/core"; +import { BlockNoteEditor } from "@blocknote/core"; +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; +import { ColorIcon } from "../../../SharedComponents/ColorPicker/components/ColorIcon"; +import { ColorPicker } from "../../../SharedComponents/ColorPicker/components/ColorPicker"; + +export const ColorStyleButton = (props: { editor: BlockNoteEditor }) => { + const setTextColor = useCallback( + (color: string) => { + props.editor.focus(); + color === "default" + ? props.editor.removeStyles({ textColor: color }) + : props.editor.addStyles({ textColor: color }); + }, + [props.editor] + ); + + const setBackgroundColor = useCallback( + (color: string) => { + props.editor.focus(); + color === "default" + ? props.editor.removeStyles({ backgroundColor: color }) + : props.editor.addStyles({ backgroundColor: color }); + }, + [props.editor] + ); + + return ( + + + ( + + )} + /> + + + + + + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx new file mode 100644 index 0000000000..c6caccc3fe --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx @@ -0,0 +1,28 @@ +import { useCallback } from "react"; +import { BlockNoteEditor } from "@blocknote/core"; +import { RiLink } from "react-icons/ri"; +import LinkToolbarButton from "../LinkToolbarButton"; +import { formatKeyboardShortcut } from "../../../utils"; + +export const CreateLinkButton = (props: { editor: BlockNoteEditor }) => { + const setLink = useCallback( + (url: string, text?: string) => { + props.editor.focus(); + props.editor.createLink(url, text); + }, + [props.editor] + ); + + return ( + + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/NestBlockButtons.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/NestBlockButtons.tsx new file mode 100644 index 0000000000..bac8f93e6c --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/NestBlockButtons.tsx @@ -0,0 +1,39 @@ +import { formatKeyboardShortcut } from "../../../utils"; +import { RiIndentDecrease, RiIndentIncrease } from "react-icons/ri"; +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; +import { BlockNoteEditor } from "@blocknote/core"; +import { useCallback } from "react"; + +export const NestBlockButton = (props: { editor: BlockNoteEditor }) => { + const nestBlock = useCallback(() => { + props.editor.focus(); + props.editor.nestBlock(); + }, [props.editor]); + + return ( + + ); +}; + +export const UnnestBlockButton = (props: { editor: BlockNoteEditor }) => { + const unnestBlock = useCallback(() => { + props.editor.focus(); + props.editor.unnestBlock(); + }, [props]); + + return ( + + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx new file mode 100644 index 0000000000..f05e5af8a6 --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx @@ -0,0 +1,61 @@ +import { + RiAlignCenter, + RiAlignJustify, + RiAlignLeft, + RiAlignRight, +} from "react-icons/ri"; +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; +import { BlockNoteEditor, DefaultBlockProps } from "@blocknote/core"; +import { useCallback } from "react"; +import { IconType } from "react-icons"; + +const icons: Record = { + left: RiAlignLeft, + center: RiAlignCenter, + right: RiAlignRight, + justify: RiAlignJustify, +}; + +export const TextAlignButton = (props: { + editor: BlockNoteEditor; + textAlignment: DefaultBlockProps["textAlignment"]; +}) => { + const setTextAlignment = useCallback( + (textAlignment: DefaultBlockProps["textAlignment"]) => { + props.editor.focus(); + + const selection = props.editor.getSelection(); + + if (selection) { + for (const block of selection.blocks) { + props.editor.updateBlock(block, { + props: { textAlignment: textAlignment }, + }); + } + } else { + props.editor.updateBlock(props.editor.getTextCursorPosition().block, { + props: { textAlignment: textAlignment }, + }); + } + }, + [props.editor] + ); + + return ( + setTextAlignment(props.textAlignment)} + isSelected={ + props.editor.getTextCursorPosition().block.props.textAlignment === + props.textAlignment + } + mainTooltip={ + props.textAlignment === "justify" + ? "Justify Text" + : "Align Text " + + props.textAlignment.slice(0, 1).toUpperCase() + + props.textAlignment.slice(1) + } + icon={icons[props.textAlignment]} + /> + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx new file mode 100644 index 0000000000..57adf480b0 --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx @@ -0,0 +1,50 @@ +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; +import { formatKeyboardShortcut } from "../../../utils"; +import { + RiBold, + RiCodeFill, + RiItalic, + RiStrikethrough, + RiUnderline, +} from "react-icons/ri"; +import { BlockNoteEditor, ToggledStyle } from "@blocknote/core"; +import { IconType } from "react-icons"; + +const shortcuts: Record = { + bold: "Mod+B", + italic: "Mod+I", + underline: "Mod+U", + strike: "Mod+Shift+X", + code: "", +}; + +const icons: Record = { + bold: RiBold, + italic: RiItalic, + underline: RiUnderline, + strike: RiStrikethrough, + code: RiCodeFill, +}; + +export const ToggledStyleButton = (props: { + editor: BlockNoteEditor; + toggledStyle: ToggledStyle; +}) => { + const toggleStyle = (style: ToggledStyle) => { + props.editor.focus(); + props.editor.toggleStyles({ [style]: true }); + }; + + return ( + toggleStyle(props.toggledStyle)} + isSelected={props.toggledStyle in props.editor.getActiveStyles()} + mainTooltip={ + props.toggledStyle.slice(0, 1).toUpperCase() + + props.toggledStyle.slice(1) + } + secondaryTooltip={formatKeyboardShortcut(shortcuts[props.toggledStyle])} + icon={icons[props.toggledStyle]} + /> + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx new file mode 100644 index 0000000000..d69c85b812 --- /dev/null +++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { BlockNoteEditor } from "@blocknote/core"; +import { + RiH1, + RiH2, + RiH3, + RiListOrdered, + RiListUnordered, + RiText, +} from "react-icons/ri"; +import { ToolbarDropdown } from "../../../SharedComponents/Toolbar/components/ToolbarDropdown"; + +export const BlockTypeDropdown = (props: { editor: BlockNoteEditor }) => { + const [block, setBlock] = useState( + props.editor.getTextCursorPosition().block + ); + + useEffect( + () => setBlock(props.editor.getTextCursorPosition().block), + [props] + ); + + return ( + { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "paragraph", + props: {}, + }); + }, + text: "Paragraph", + icon: RiText, + isSelected: block.type === "paragraph", + }, + { + onClick: () => { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "heading", + props: { level: "1" }, + }); + }, + text: "Heading 1", + icon: RiH1, + isSelected: block.type === "heading" && block.props.level === "1", + }, + { + onClick: () => { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "heading", + props: { level: "2" }, + }); + }, + text: "Heading 2", + icon: RiH2, + isSelected: block.type === "heading" && block.props.level === "2", + }, + { + onClick: () => { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "heading", + props: { level: "3" }, + }); + }, + text: "Heading 3", + icon: RiH3, + isSelected: block.type === "heading" && block.props.level === "3", + }, + { + onClick: () => { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "bulletListItem", + props: {}, + }); + }, + text: "Bullet List", + icon: RiListUnordered, + isSelected: block.type === "bulletListItem", + }, + { + onClick: () => { + props.editor.focus(); + props.editor.updateBlock(block, { + type: "numberedListItem", + props: {}, + }); + }, + text: "Numbered List", + icon: RiListOrdered, + isSelected: block.type === "numberedListItem", + }, + ]} + /> + ); +}; diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx index 3e3ab4ff08..dfacb7f90a 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx @@ -1,302 +1,38 @@ -import { Menu } from "@mantine/core"; -import { - RiAlignCenter, - RiAlignLeft, - RiAlignRight, - RiBold, - RiH1, - RiH2, - RiH3, - RiIndentDecrease, - RiIndentIncrease, - RiItalic, - RiLink, - RiListOrdered, - RiListUnordered, - RiStrikethrough, - RiText, - RiUnderline, -} from "react-icons/ri"; -import { Block, PartialBlock } from "@blocknote/core"; +import { FC } from "react"; +import { BlockNoteEditor } from "@blocknote/core"; import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; -import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton"; -import { ToolbarDropdown } from "../../SharedComponents/Toolbar/components/ToolbarDropdown"; -import { formatKeyboardShortcut } from "../../utils"; -import LinkToolbarButton from "./LinkToolbarButton"; -import { ColorPicker } from "../../SharedComponents/ColorPicker/components/ColorPicker"; -import { ColorIcon } from "../../SharedComponents/ColorPicker/components/ColorIcon"; - -export type FormattingToolbarProps = { - boldIsActive: boolean; - toggleBold: () => void; - italicIsActive: boolean; - toggleItalic: () => void; - underlineIsActive: boolean; - toggleUnderline: () => void; - strikeIsActive: boolean; - toggleStrike: () => void; - hyperlinkIsActive: boolean; - activeHyperlinkUrl: string; - activeHyperlinkText: string; - setHyperlink: (url: string, text?: string) => void; - - textColor: string; - setTextColor: (color: string) => void; - backgroundColor: string; - setBackgroundColor: (color: string) => void; - textAlignment: "left" | "center" | "right" | "justify"; - setTextAlignment: ( - textAlignment: "left" | "center" | "right" | "justify" - ) => void; - - canIncreaseBlockIndent: boolean; - increaseBlockIndent: () => void; - canDecreaseBlockIndent: boolean; - decreaseBlockIndent: () => void; - - block: Block; - updateBlock: (updatedBlock: PartialBlock) => void; -}; - -// TODO: add list options, indentation -export const FormattingToolbar = (props: FormattingToolbarProps) => { - const getActiveMarks = () => { - const activeMarks = new Set(); - - props.boldIsActive && activeMarks.add("bold"); - props.italicIsActive && activeMarks.add("italic"); - props.underlineIsActive && activeMarks.add("underline"); - props.strikeIsActive && activeMarks.add("strike"); - props.hyperlinkIsActive && activeMarks.add("link"); - - return activeMarks; - }; - - const getActiveBlock = () => { - if (props.block.type === "heading") { - if (props.block.props.level === "1") { - return { - text: "Heading 1", - icon: RiH1, - }; - } - - if (props.block.props.level === "2") { - return { - text: "Heading 2", - icon: RiH2, - }; - } - - if (props.block.props.level === "3") { - return { - text: "Heading 3", - icon: RiH3, - }; - } - } - - if (props.block.type === "bulletListItem") { - return { - text: "Bullet List", - icon: RiListUnordered, - }; - } - - if (props.block.type === "numberedListItem") { - return { - text: "Numbered List", - icon: RiListOrdered, - }; - } - - return { - text: "Paragraph", - icon: RiText, - }; - }; - - const activeMarks = getActiveMarks(); - const activeBlock = getActiveBlock(); - +import { BlockTypeDropdown } from "./DefaultDropdowns/BlockTypeDropdown"; +import { ToggledStyleButton } from "./DefaultButtons/ToggledStyleButton"; +import { TextAlignButton } from "./DefaultButtons/TextAlignButton"; +import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton"; +import { + NestBlockButton, + UnnestBlockButton, +} from "./DefaultButtons/NestBlockButtons"; +import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton"; + +export const FormattingToolbar: FC<{ editor: BlockNoteEditor }> = (props: { + editor: BlockNoteEditor; +}) => { return ( - - props.updateBlock({ - type: "paragraph", - props: {}, - }), - text: "Paragraph", - icon: RiText, - isSelected: props.block.type === "paragraph", - }, - { - onClick: () => - props.updateBlock({ - type: "heading", - props: { level: "1" }, - }), - text: "Heading 1", - icon: RiH1, - isSelected: - props.block.type === "heading" && props.block.props.level === "1", - }, - { - onClick: () => - props.updateBlock({ - type: "heading", - props: { level: "2" }, - }), - text: "Heading 2", - icon: RiH2, - isSelected: - props.block.type === "heading" && props.block.props.level === "2", - }, - { - onClick: () => - props.updateBlock({ - type: "heading", - props: { level: "3" }, - }), - text: "Heading 3", - icon: RiH3, - isSelected: - props.block.type === "heading" && props.block.props.level === "3", - }, - { - onClick: () => - props.updateBlock({ - type: "bulletListItem", - props: {}, - }), - text: "Bullet List", - icon: RiListUnordered, - isSelected: props.block.type === "bulletListItem", - }, - { - onClick: () => - props.updateBlock({ - type: "numberedListItem", - props: {}, - }), - text: "Numbered List", - icon: RiListOrdered, - isSelected: props.block.type === "numberedListItem", - }, - ]} - /> - - - - - - props.setTextAlignment("left")} - isSelected={props.textAlignment === "left"} - mainTooltip={"Align Text Left"} - icon={RiAlignLeft} - /> - - props.setTextAlignment("center")} - isSelected={props.textAlignment === "center"} - mainTooltip={"Align Text Center"} - icon={RiAlignCenter} - /> + - props.setTextAlignment("right")} - isSelected={props.textAlignment === "right"} - mainTooltip={"Align Text Right"} - icon={RiAlignRight} - /> + + + + - - - ( - - )} - /> - - - - - + + + - + - + + - - {/* { - const comment = this.props.commentStore.createComment(); - props.editor.chain().focus().setComment(comment.id).run(); - }} - styleDetails={comment} - /> */} + ); }; diff --git a/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx b/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx index 912956a91f..5294bd0003 100644 --- a/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx +++ b/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx @@ -20,7 +20,8 @@ export const LinkToolbarButton = (props: HyperlinkButtonProps) => { const [creationMenu, setCreationMenu] = useState(); const [creationMenuOpen, setCreationMenuOpen] = useState(false); - const ref = useRef(null); + const buttonRef = useRef(null); + const menuRef = useRef(null); // TODO: review code; does this pattern still make sense? const updateCreationMenu = useCallback(() => { @@ -33,17 +34,22 @@ export const LinkToolbarButton = (props: HyperlinkButtonProps) => { props.setHyperlink(url, text); setCreationMenuOpen(false); }} + ref={menuRef} /> ); }, [props]); const handleClick = useCallback( (event: MouseEvent) => { - if (ref.current?.contains(event.target as HTMLElement)) { + if (buttonRef.current?.contains(event.target as HTMLElement)) { setCreationMenuOpen(!creationMenuOpen); return; } + if (menuRef.current?.contains(event.target as HTMLElement)) { + return; + } + setCreationMenuOpen(false); }, [creationMenuOpen] @@ -67,7 +73,7 @@ export const LinkToolbarButton = (props: HyperlinkButtonProps) => { mainTooltip={props.mainTooltip} secondaryTooltip={props.secondaryTooltip} icon={props.icon} - ref={ref} + ref={buttonRef} /> ); diff --git a/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx index cd5898dc8a..d2c2eee650 100644 --- a/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx @@ -1,5 +1,5 @@ import { createStyles, Stack } from "@mantine/core"; -import { useState } from "react"; +import { forwardRef, useState } from "react"; import { RiLink, RiText } from "react-icons/ri"; import { EditHyperlinkMenuItem } from "./EditHyperlinkMenuItem"; @@ -13,7 +13,10 @@ export type EditHyperlinkMenuProps = { * Menu which opens when editing an existing hyperlink or creating a new one. * Provides input fields for setting the hyperlink URL and title. */ -export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => { +export const EditHyperlinkMenu = forwardRef< + HTMLDivElement, + EditHyperlinkMenuProps +>((props, ref) => { const { classes } = createStyles({ root: {} })(undefined, { name: "EditHyperlinkMenu", }); @@ -22,7 +25,7 @@ export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => { const [title, setTitle] = useState(props.text); return ( - + { /> ); -}; +}); diff --git a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx index a97a6539a4..40e4009509 100644 --- a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx +++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx @@ -1,29 +1,27 @@ import { Menu } from "@mantine/core"; -import { IconType } from "react-icons"; -import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget"; import { MouseEvent } from "react"; +import { IconType } from "react-icons"; import { ToolbarDropdownItem } from "./ToolbarDropdownItem"; +import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget"; export type ToolbarDropdownProps = { - text: string; - icon?: IconType; items: Array<{ onClick?: (e: MouseEvent) => void; text: string; icon?: IconType; isSelected?: boolean; - children?: any; isDisabled?: boolean; }>; - children?: any; isDisabled?: boolean; }; export function ToolbarDropdown(props: ToolbarDropdownProps) { + const activeItem = props.items.filter((p) => p.isSelected)[0]; + return ( - + - + {props.items.map((item) => ( diff --git a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx index bf60c106a8..9ec0789a8e 100644 --- a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx +++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx @@ -4,11 +4,10 @@ import { TiTick } from "react-icons/ti"; import { MouseEvent } from "react"; export type ToolbarDropdownItemProps = { - onClick?: (e: MouseEvent) => void; text: string; icon?: IconType; + onClick?: (e: MouseEvent) => void; isSelected?: boolean; - children?: any; isDisabled?: boolean; }; diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 0a98acf0d2..802cfddd45 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -1,7 +1,11 @@ import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core"; import { DependencyList, useEffect, useState } from "react"; -import { ReactBlockSideMenuFactory } from "../BlockSideMenu/BlockSideMenuFactory"; -import { ReactFormattingToolbarFactory } from "../FormattingToolbar/FormattingToolbarFactory"; +import { + createReactBlockSideMenuFactory, +} from "../BlockSideMenu/BlockSideMenuFactory"; +import { + createReactFormattingToolbarFactory, +} from "../FormattingToolbar/FormattingToolbarFactory"; import { ReactHyperlinkToolbarFactory } from "../HyperlinkToolbar/HyperlinkToolbarFactory"; import { ReactSlashMenuFactory } from "../SlashMenu/SlashMenuFactory"; import { defaultReactSlashMenuItems } from "../SlashMenu/defaultReactSlashMenuItems"; @@ -37,10 +41,10 @@ export const useBlockNote = ( newOptions = { ...newOptions, uiFactories: { - formattingToolbarFactory: ReactFormattingToolbarFactory, + formattingToolbarFactory: createReactFormattingToolbarFactory(), hyperlinkToolbarFactory: ReactHyperlinkToolbarFactory, slashMenuFactory: ReactSlashMenuFactory, - blockSideMenuFactory: ReactBlockSideMenuFactory, + blockSideMenuFactory: createReactBlockSideMenuFactory(), }, }; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9c8c0d3bdd..c29a62000a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,10 +1,30 @@ // TODO: review directories export * from "./BlockNoteView"; + export * from "./BlockSideMenu/BlockSideMenuFactory"; +export * from "./BlockSideMenu/components/DragHandleMenu"; +export * from "./BlockSideMenu/components/DragHandleMenuItem"; +export * from "./BlockSideMenu/components/DefaultButtons/RemoveBlockButton"; +export * from "./BlockSideMenu/components/DefaultButtons/BlockColorsButton"; + export * from "./FormattingToolbar/FormattingToolbarFactory"; +export * from "./FormattingToolbar/components/FormattingToolbar"; +export * from "./FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown"; +export * from "./FormattingToolbar/components/DefaultButtons/ToggledStyleButton"; +export * from "./FormattingToolbar/components/DefaultButtons/TextAlignButton"; +export * from "./FormattingToolbar/components/DefaultButtons/ColorStyleButton"; +export * from "./FormattingToolbar/components/DefaultButtons/NestBlockButtons"; +export * from "./FormattingToolbar/components/DefaultButtons/CreateLinkButton"; + export * from "./hooks/useBlockNote"; export * from "./hooks/useEditorForceUpdate"; + export * from "./HyperlinkToolbar/HyperlinkToolbarFactory"; + export * from "./SlashMenu/SlashMenuFactory"; export * from "./SlashMenu/ReactSlashMenuItem"; export * from "./SlashMenu/defaultReactSlashMenuItems"; + +export * from "./SharedComponents/Toolbar/components/Toolbar"; +export * from "./SharedComponents/Toolbar/components/ToolbarButton"; +export * from "./SharedComponents/Toolbar/components/ToolbarDropdown"; diff --git a/packages/website/docs/docs/cursor-selections.md b/packages/website/docs/docs/cursor-selections.md index c0ae064f02..1af02af321 100644 --- a/packages/website/docs/docs/cursor-selections.md +++ b/packages/website/docs/docs/cursor-selections.md @@ -2,10 +2,6 @@ If you want to know which block(s) the user is currently editing, you can do so using cursor positions and selections. -## Cursor Positions - -BlockNote keeps track of the text cursor position in the editor and exposes functions that let you retrieve it or change it. - ## Text Cursor The text cursor is the blinking vertical line you see when typing in the editor. BlockNote uses `TextCursorPosition` objects to give you information about the block it's in as well as those around it: @@ -67,7 +63,9 @@ editor.setTextCursorPosition(targetBlock, placement); Throws an error if the target block could not be found. -**Demo** +### Demo: Highlighting Block Containing the Text Cursor + +If you need a visualization for which block contains the text cursor, the demo below highlights it in blue in real time. ::: sandbox {template=react-ts} @@ -92,7 +90,7 @@ export default function App() { ) { // If the block is currently hovered by the text cursor, makes its // background blue if it isn't already. - editor.updateBlock(hoveredBlock, { + editor.updateBlock(block, { props: {backgroundColor: "blue"}, }); } else if ( @@ -105,6 +103,8 @@ export default function App() { props: {backgroundColor: "default"}, }); } + + return true; }); } }) @@ -114,4 +114,100 @@ export default function App() { } ``` +::: + +## Selections + +When you highlight content using the mouse or keyboard, this is called a selection. BlockNote uses `Selection` objects to show which blocks the current selection spans across: + +```typescript +type Selection = { + blocks: Block[]; +} +``` + +`blocks:` The blocks currently spanned by the selection, including nested blocks. + +### Getting Selection + +You can get a snapshot of the current selection using the following call: + +```typescript +// Definition +class BlockNoteEditor { +... + public getSelection(): Selection | undefined; +... +} + +// Usage +const selection = editor.getSelection(); +``` + +`returns:` A snapshot of the current selection, or `undefined` if no selection is active. + +### Demo: Highlighting Blocks Spanned by Selection + +If you need a visualization for which block contains the text cursor, the demo below highlights it in blue in real time. + +::: sandbox {template=react-ts} + +```typescript /App.tsx +import { BlockNoteEditor, Block } from "@blocknote/core"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; +import "@blocknote/core/style.css"; + +export default function App() { + // Creates a new editor instance. + const editor: BlockNoteEditor | null = useBlockNote({ + // Listens for when the text cursor position changes. + onTextCursorPositionChange: (editor: BlockNoteEditor) => { + // Gets the blocks currently spanned by the selection. + const selectedBlocks: Block[] | undefined = editor.getSelection()?.blocks; + // Converts array of blocks to set of block IDs for more efficient comparison. + const selectedBlockIds: Set = new Set( + selectedBlocks?.map((block) => block.id) || [] + ); + + // Traverses all blocks. + editor.forEachBlock((block: Block) => { + // If no selection is active, resets the background color of each block. + if (selectedBlockIds.size === 0) { + editor.updateBlock(block, { + props: { backgroundColor: "default" }, + }); + + return true; + } + + if ( + selectedBlockIds.has(block.id) && + block.props.backgroundColor !== "blue" + ) { + // If the block is currently spanned by the selection, makes its + // background blue if it isn't already. + editor.updateBlock(block, { + props: { backgroundColor: "blue" }, + }); + } else if ( + !selectedBlockIds.has(block.id) && + block.props.backgroundColor === "blue" + ) { + // If the block is not currently spanned by the selection, resets + // its background if it's blue. + editor.updateBlock(block, { + props: { backgroundColor: "default" }, + }); + } + + return true; + }); + }, + }); + + // Renders the editor instance. + return ; +} +``` + ::: \ No newline at end of file diff --git a/packages/website/docs/docs/formatting-toolbar.md b/packages/website/docs/docs/formatting-toolbar.md index a394d68032..b6cb614cc6 100644 --- a/packages/website/docs/docs/formatting-toolbar.md +++ b/packages/website/docs/docs/formatting-toolbar.md @@ -1,7 +1,198 @@ # Formatting Toolbar -Coming soon! +The Formatting Toolbar appears whenever you highlight text in the editor, and is mainly used for styling. - +image + +## Custom Formatting Toolbar + +You can create a custom Formatting Toolbar using a React component. This component should take the following props: + +```typescript +type CustomFormattingToolbarProps = { + editor: BlockNoteEditor +}; + +const CustomFormattingToolbar = (props: CustomFormattingToolbarProps): JSX.Element => ...; +``` + +You can then tell BlockNote to use your custom Formatting Toolbar using the `uiFactories` option in `useBlockNote`, but you first have to turn it into a `FormattingToolbarFactory`. Fortunately, you can easily do this using the `createReactFormattingToolbarFactory` function: + +```typescript +const editor = useBlockNote({ + uiFactories: { + formattingToolbarFactory: createReactFormattingToolbarFactory( + CustomFormattingToolbar + ), + }, +}); +``` + +## Default Items + +It might seem daunting to create your own Formatting Toolbar from scratch, which is why BlockNote provides React components for everything you see in the default layout - both the toolbar itself and the items in it. Below are all the default components you can use to build your custom toolbar: + +```typescript +// Toolbar which wraps all the items. +type ToolbarProps = { + children: ReactNode; +}; +const Toolbar = (props: ToolbarProps) => ...; + +// Dropdown which changes the block type. +type BlockTypeDropdownProps = { + editor: BlockNoteEditor +}; +const BlockTypeDropdown = (props: BlockTypeDropdownProps) => ...; + +// Button which toggles a simple style on the highlighted text. +type ToggledStyleButtonProps = { + editor: BlockNoteEditor; + toggledStyle: "bold" | "italic" | "underline" | "strike" | "code"; +}; +const ToggledStyleButton = (props: ToggledStyleButtonProps) => ...; + +// Button which sets the text alignment on the block. +type TextAlignButtonProps = { + editor: BlockNoteEditor; + textAlignment: "left" | "center" | "right" | "center"; +}; +const TextAlignButton = (props: TextAlignButtonProps) => ...; + +// Button which opens a dropdown on hover. The dropdown lets you set the +// highlighted text's color. +type ColorStyleButtonProps = { + editor: BlockNoteEditor +}; +const ColorStyleButton = (props: ColorStyleButtonProps) => ...; + +// Button which nests the block, if it can be nested. +type NestBlockButtonProps = { + editor: BlockNoteEditor +}; +const NestBlockButton = (props: NestBlockButtonProps) => ...; + +// Button which unnests the block, if it can be nested. +type UnestBlockButtonProps = { + editor: BlockNoteEditor +}; +const UnestBlockButton = (props: UnestBlockButtonProps) => ...; + +// Button which opens a dialog box to create a new link. +type CreateLinkButtonProps = { + editor: BlockNoteEditor +}; +const CreateLinkButton = (props: CreateLinkButtonProps) => ...; +``` + +## Custom Items + +BlockNote also provides components that you can use to make your own toolbar items, which match BlockNote's UI styling: + +```typescript +// Custom dropdown. +type ToolbarDropdownProps = { + // Array representing the items in the dropdown. + items: Array<{ + // Item name/text. + text: string; + // Icon next to the text. + icon?: IconType; + // Function to execute on click. + onClick?: (e: MouseEvent) => void; + // Condition for when the item is selected/active. + isSelected?: boolean; + // Whether the item should be clickable. + isDisabled?: boolean; + }>; + // Whether the dropdown should be clickable. + isDisabled?: boolean; +}; +const ToolbarDropdown = (props: ToolbarDropdownProps): JSX.Element => ...; + +// Custom button. +type ToolbarButtonProps = { + // Main tooltip, which is shown on hover. + mainTooltip: string; + // Secondary tooltip, usually showing the keyboard shortcut. + secondaryTooltip?: string; + // Icon for the button. + icon?: IconType; + // Function to execute on click. + onClick?: (e: MouseEvent) => void; + // Condition for when the item is selected/active. + isSelected?: boolean; + // Whether the item should be clickable. + isDisabled?: boolean; + // Child components, usually just the button text. If no children are + // given, make sure to provide an icon. + children?: any; +}; +export const ToolbarButton = (props: ToolbarButtonProps) => ...; +``` + +## Demo: Simple Custom Formatting Toolbar + +The example below shows a basic custom formatting toolbar with four items. The first three are default items to toggle bold, italic, and underline, while the last one is a custom button which toggles the text and background colors to be blue. + +::: sandbox {template=react-ts} + +```typescript /App.tsx +import { BlockNoteEditor } from "@blocknote/core"; +import { + BlockNoteView, + createReactFormattingToolbarFactory, + ToggledStyleButton, + Toolbar, + ToolbarButton, + useBlockNote, +} from "@blocknote/react"; +import "@blocknote/core/style.css"; + +const CustomFormattingToolbar = (props: { editor: BlockNoteEditor }) => { + return ( + + {/*Default button to toggle bold.*/} + + {/*Default button to toggle italic.*/} + + {/*Default button to toggle underline.*/} + + {/*Custom button to toggle blue text & background color.*/} + { + props.editor.toggleStyles({ + textColor: "blue", + backgroundColor: "blue", + }); + }} + isSelected={ + props.editor.getActiveStyles().textColor === "blue" && + props.editor.getActiveStyles().backgroundColor === "blue" + }> + Blue + + + ); +}; + +export default function App() { + // Creates a new editor instance. + const editor: BlockNoteEditor = useBlockNote({ + uiFactories: { + // Makes the editor instance use the custom toolbar. + formattingToolbarFactory: createReactFormattingToolbarFactory( + CustomFormattingToolbar + ), + }, + }); + + // Renders the editor instance. + return ; +} +``` + +::: + +If you're unsure about what's happening inside `onClick` and `isSelected`, head to [Introduction to Blocks](/docs/blocks), which will guide you through reading & manipulating blocks in the editor using code. \ No newline at end of file diff --git a/packages/website/docs/docs/inline-content.md b/packages/website/docs/docs/inline-content.md index 20fb7a729e..377105e103 100644 --- a/packages/website/docs/docs/inline-content.md +++ b/packages/website/docs/docs/inline-content.md @@ -1,21 +1,12 @@ # Inline Content -An array of `InlineContent` objects is used to describe the rich text content inside a block. Inline content can either be styled text or a link, and we'll go over both these in the next section. +An array of `InlineContent` objects is used to describe the rich text content inside a block. Inline content can either be styled text or a link, and we'll go over both these in the upcoming sections. ## Styled Text -Styled text is used to display pieces of text with markup, or styling, and is defined using a `StyledText` object: +Styled text is a type of `InlineContent` used to display pieces of text with styles, and is defined using a `StyledText` object: ```typescript -type Styles = { - bold?: true; - italic?: true; - underline?: true; - strikethrough?: true; - textColor?: string; - backgroundColor?: string; -}; - type StyledText = { type: "text"; text: string; @@ -23,13 +14,28 @@ type StyledText = { }; ``` -`text:` The text that should be displayed with the markup in `styles`. +`text:` The displayed text. + +`styles:` The styles that are applied to the text. + +### Styles Object -`styles:` An object containing the markup/styling that should be applied to the text. +`StyledText` supports a variety of styles, including bold, underline, and text color, which are represented using a `Styles` object: + +```typescript +type Styles = Partial<{ + bold: true; + italic: true; + underline: true; + strikethrough: true; + textColor: string; + backgroundColor: string; +}>; +``` ## Links -Links are used to create clickable hyperlinks that go to some URL, and are made up of pieces of styled text. They're defined using `Link` objects: +Links are a type of `InlineContent` used to create clickable hyperlinks that go to some URL, and are made up of `StyledText`. They're defined using `Link` objects: ```typescript type Link = { @@ -39,6 +45,128 @@ type Link = { }; ``` -`content:` The Styled Text that should be used to display the link. +`content:` The styled text used to display the link. + +`href:` The URL that opens when clicking the link. + +## Editor Functions + +While `InlineContent` objects are used to describe a block's content, they can be cumbersome to work with directly. Therefore, BlockNote exposes functions which make it easier to edit block contents. + +### Accessing Styles + +You can get the styles at the current [Text Cursor Position](/docs/cursor-selections#text-cursor) using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public getActiveStyles(): Styles; +... +} + +// Usage +const styles = editor.getActiveStyles(); +``` + +If a [Selection](/docs/cursor-selections#selections) is active, this function returns the active styles at the end of the selection. + +### Adding Styles + +You can add styles to the currently selected text using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public addStyles(styles: Styles): void; +... +} + +// Usage +editor.addStyles({ textColor: "red" }); +``` + +### Removing Styles + +You can remove styles from the currently selected text using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public removeStyles(styles: Styles): void; +... +} + +// Usage +editor.removeStyles({ bold: true }); +``` + +### Toggling Styles + +You can toggle styles on the currently selected text using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public toggleStyles(styles: Styles): void; +... +} + +// Usage +editor.toggleStyles({ bold: true, italic: true }); +``` + +### Accessing Selected Text + +You can get the currently selected text using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public getSelectedText(): string; +... +} + +// Usage +const text = editor.getSelectedText(); +``` + +### Accessing Selected Link + +You can get the URL of the link in the current selection the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public getSelectedLink(): string | undefined; +... +} + +// Usage +const linkUrl = editor.getSelectedLink(); +``` + +If there are multiple links in the selection, this function only returns the last one's URL. If there are no links, returns `undefined`. + +### Creating a Link + +You can create a new link using the following function: + +```typescript +// Definition +class BlockNoteEditor { +... + public createLink(url: string, text?: string): void; +... +} + +// Usage +editor.createLink("https://www.blocknotejs.org/", "BlockNote"); +``` -`href:` The URL that should be opened when clicking the link. +If a [Selection](/docs/cursor-selections#selections) is active, the new link will replace the currently selected content. diff --git a/packages/website/docs/docs/manipulating-blocks.md b/packages/website/docs/docs/manipulating-blocks.md index 659cce4f9b..e874465334 100644 --- a/packages/website/docs/docs/manipulating-blocks.md +++ b/packages/website/docs/docs/manipulating-blocks.md @@ -196,3 +196,67 @@ editor.replaceBlocks(blocksToRemove, blocksToInsert) `blocksToInsert:` An array of [partial blocks](#partial-blocks) that the existing ones should be replaced with. If the blocks that should be removed are not adjacent or are at different nesting levels, `blocksToInsert` will be inserted at the position of the first block in `blocksToRemove`. Throws an error if any of the blocks to remove could not be found. + +## Nesting & Un-nesting Blocks + +BlockNote also provides functions to nest & un-nest the block containing the [Text Cursor](/docs/cursor-selections#text-cursor). + +### Nesting Blocks + +You can check whether the block containing the [Text Cursor](/docs/cursor-selections#text-cursor) can be nested (i.e. if there is a block above it at the same nesting level) using the following call: + +```typescript +// Definition +class BlockNoteEditor { +... + public canNestBlock(): boolean; +... +} + +// Usage +const canNestBlock = editor.canNestBlock(); +``` + +Then nest the block with another call: + +```typescript +// Definition +class BlockNoteEditor { +... + public nestBlock(): void; +... +} + +// Usage +editor.nestBlock(); +``` + +### Un-nesting Blocks + +You can also check whether the block containing the [Text Cursor](/docs/cursor-selections#text-cursor) can be un-nested (i.e. if it's nested in another block) using the following call: + +```typescript +// Definition +class BlockNoteEditor { +... + public canUnnestBlock(): boolean; +... +} + +// Usage +const canUnnestBlock = editor.canUnnestBlock(); +``` + +Then un-nest the block with another call: + +```typescript +// Definition +class BlockNoteEditor { +... + public unnestBlock(): void; +... +} + +// Usage +editor.unnestBlock(); +``` \ No newline at end of file diff --git a/packages/website/docs/docs/side-menu.md b/packages/website/docs/docs/side-menu.md index 7b6e4e22b3..640a4e0e8b 100644 --- a/packages/website/docs/docs/side-menu.md +++ b/packages/website/docs/docs/side-menu.md @@ -1,8 +1,133 @@ -# Side Menu +# Block Side Menu -Coming soon! +The Block Side Menu appears whenever you hover over a block, and is used to drag & drop the block as well as add new ones below it. - +image + +You can also click the drag handle in the Block Side Menu (`⠿`) to open the Drag Handle Menu. + +image + +## Custom Drag Handle Menu + +BlockNote lets you customize which items appear in the Drag Handle Menu. Have a look at the example below, in which the color picker item is replaced with a custom item that opens an alert. + +::: sandbox {template=react-ts} + +```typescript /App.tsx +import { Block, BlockNoteEditor } from "@blocknote/core"; +import { + BlockNoteView, + createReactBlockSideMenuFactory, + DragHandleMenu, + DragHandleMenuItem, + RemoveBlockButton, + useBlockNote, +} from "@blocknote/react"; +import "@blocknote/core/style.css"; + +const CustomDragHandleMenu = (props: { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}) => { + return ( + + {/*Default button to remove the block.*/} + + Delete + + {/*Custom item which opens an alert when clicked.*/} + { + window.alert("Button Pressed!"); + props.closeMenu(); + }}> + Open Alert + + + ); +}; + +export default function App() { + // Creates a new editor instance. + const editor: BlockNoteEditor = useBlockNote({ + uiFactories: { + // Makes the editor instance use the custom menu. + blockSideMenuFactory: + createReactBlockSideMenuFactory(CustomDragHandleMenu), + }, + }); + // Renders the editor instance. + return ; +} +``` + +::: + +Let's look at how this is done. We first need to create a custom Drag Handle Menu using a React component. This component should take the following props: + +```typescript +type CustomDragHandleMenuProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}; +const CustomDragHandleMenu = (props: CustomDragHandleMenuProps): JSX.Element => ...; +``` + +You can then tell BlockNote to use your custom Drag Handle Menu using the `uiFactories` option in `useBlockNote`, but you first have to turn it into a `SideMenuFactory` that uses it. Fortunately, you can easily do this using the `createReactSideMenuFactory` function: + +```typescript +const editor = useBlockNote({ + uiFactories: { + blockSideMenuFactory: createReactBlockSideMenuFactory( + CustomBlockSideMenu + ), + }, +}); +``` + +## Default Items + +It might seem daunting to create your own Drag Handle Menu from scratch, which is why BlockNote provides React components for everything you see in the default layout - both the menu itself and the items in it. Below are all the default components you can use to build your custom menu: + +```typescript +// Menu which wraps all the items. +type BlockSideMenuProps = { + children: ReactNode +} +const BlockSideMenu = (props: BlockSideMenuProps) => ...; + +// Button which removes the block. +type RemoveBlockButtonProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; + children: ReactNode; +}; +const RemoveBlockButton = (props: RemoveBlockButtonProps) => ...; + +// Button which opens a dropdown on hover. The dropdown lets you set the block's color. +type BlockColorsButtonProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; + children: ReactNode; +}; +const BlockColorsButton = (props: BlockColorsButtonProps) => ...; +``` + +## Custom Items + +BlockNote also provides components that you can use to make your own menu items, which match BlockNote's UI styling: + +```typescript +// Also includes all props of button elements, e.g. onClick. +type DragHandleMenuItemProps = { + // Closes the menu when called. + closeMenu: () => void; +}; +export const DragHandleMenuItem = (props: DragHandleMenuItemProps) => ...; +``` \ No newline at end of file diff --git a/packages/website/docs/public/img/screenshots/drag_handle_menu.png b/packages/website/docs/public/img/screenshots/drag_handle_menu.png new file mode 100644 index 0000000000..7ee2887b94 Binary files /dev/null and b/packages/website/docs/public/img/screenshots/drag_handle_menu.png differ diff --git a/packages/website/docs/public/img/screenshots/formatting_toolbar.png b/packages/website/docs/public/img/screenshots/formatting_toolbar.png new file mode 100644 index 0000000000..b4ca6d2f76 Binary files /dev/null and b/packages/website/docs/public/img/screenshots/formatting_toolbar.png differ diff --git a/packages/website/docs/public/img/screenshots/side_menu.png b/packages/website/docs/public/img/screenshots/side_menu.png new file mode 100644 index 0000000000..9b5687bc8b Binary files /dev/null and b/packages/website/docs/public/img/screenshots/side_menu.png differ diff --git a/packages/website/docs/public/img/screenshots/slash_menu.png b/packages/website/docs/public/img/screenshots/slash_menu.png index 469585376c..d7bdf09471 100644 Binary files a/packages/website/docs/public/img/screenshots/slash_menu.png and b/packages/website/docs/public/img/screenshots/slash_menu.png differ diff --git a/tests/end-to-end/indentation/indentation.test.ts b/tests/end-to-end/indentation/indentation.test.ts index 966ecac29d..46916c415b 100644 --- a/tests/end-to-end/indentation/indentation.test.ts +++ b/tests/end-to-end/indentation/indentation.test.ts @@ -1,10 +1,10 @@ import { test } from "../../setup/setupScript"; import { BASE_URL, - DECREASE_INDENT_BUTTON_SELECTOR, + UNNEST_BLOCK_BUTTON_SELECTOR, H_THREE_BLOCK_SELECTOR, H_TWO_BLOCK_SELECTOR, - INDENT_BUTTON_SELECTOR, + NEST_BLOCK_BUTTON_SELECTOR, } from "../../utils/const"; import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor"; import { insertHeading, insertParagraph } from "../../utils/copypaste"; @@ -33,7 +33,7 @@ test.describe("Check Block Indentation Functionality", () => { await page.mouse.click(x + 10, y + height / 2, { clickCount: 2 }); - await page.locator(INDENT_BUTTON_SELECTOR).click(); + await page.locator(NEST_BLOCK_BUTTON_SELECTOR).click(); await page.waitForTimeout(350); await page.mouse.click(x + 10, y + height / 2); @@ -62,7 +62,7 @@ test.describe("Check Block Indentation Functionality", () => { await page.mouse.click(x + 10, y + height / 2, { clickCount: 2 }); - await page.locator(DECREASE_INDENT_BUTTON_SELECTOR).click(); + await page.locator(UNNEST_BLOCK_BUTTON_SELECTOR).click(); await page.waitForTimeout(350); await page.mouse.click(x + 10, y + height / 2); @@ -101,7 +101,7 @@ test.describe("Check Block Indentation Functionality", () => { ); await page.keyboard.up("Shift"); - await page.locator(INDENT_BUTTON_SELECTOR).click(); + await page.locator(NEST_BLOCK_BUTTON_SELECTOR).click(); await page.waitForTimeout(350); await page.mouse.click( @@ -143,7 +143,7 @@ test.describe("Check Block Indentation Functionality", () => { ); await page.keyboard.up("Shift"); - await page.locator(DECREASE_INDENT_BUTTON_SELECTOR).click(); + await page.locator(UNNEST_BLOCK_BUTTON_SELECTOR).click(); await page.waitForTimeout(350); await page.mouse.click( diff --git a/tests/utils/const.ts b/tests/utils/const.ts index 3f8e7c09e7..874c89dcee 100644 --- a/tests/utils/const.ts +++ b/tests/utils/const.ts @@ -27,7 +27,7 @@ export const TEXT_COLOR_SELECTOR = (color: string) => export const BACKGROUND_COLOR_SELECTOR = (color: string) => `[data-test="background-color-${color}"]`; export const ALIGN_TEXT_RIGHT_BUTTON_SELECTOR = `[data-test="alignTextRight"]`; -export const INDENT_BUTTON_SELECTOR = `[data-test="indent"]`; -export const DECREASE_INDENT_BUTTON_SELECTOR = `[data-test="decreaseIndent"]`; +export const NEST_BLOCK_BUTTON_SELECTOR = `[data-test="nestBlock"]`; +export const UNNEST_BLOCK_BUTTON_SELECTOR = `[data-test="unnestBlock"]`; export const TYPE_DELAY = 10;