Skip to content

feat: customize formatting toolbar and sidemenu #142

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
65a058f
simplify formattingtoolbar
YousefED Mar 17, 2023
1b7a745
Fixed React component types and added customizable formatting toolbar…
matthewlipski Mar 29, 2023
7aa833e
Finished formatting toolbar customization with old props
matthewlipski Mar 30, 2023
4d44c84
Changed formatting toolbar props to use BlockNoteEditor
matthewlipski Mar 31, 2023
3a5a653
Fixed text alignment with basic selection object
matthewlipski Mar 31, 2023
e91708c
Fixed block nesting tests
matthewlipski Mar 31, 2023
adde0ab
Merge branch 'main' into custom-formattingtoolbar
matthewlipski Mar 31, 2023
f446be8
Removed multiple block shorthand for updateBlock
matthewlipski Mar 31, 2023
3c02d68
Merge remote-tracking branch 'origin/custom-formattingtoolbar' into c…
matthewlipski Mar 31, 2023
f4292e5
Added comments
matthewlipski Mar 31, 2023
4074c2c
Removed redundant useCallback hooks
matthewlipski Apr 3, 2023
298edfd
Split `getActiveLink` to get text & url separately
matthewlipski Apr 11, 2023
d08a436
Removed unnecessary functions and `useCallback` hooks
matthewlipski Apr 11, 2023
f2fa309
removed unnecessary `focus` calls
matthewlipski Apr 11, 2023
8426484
Merge branch 'main' into custom-formattingtoolbar
matthewlipski Apr 11, 2023
4a1c239
Small fix
matthewlipski Apr 12, 2023
d888717
Inline code style fix
matthewlipski Apr 12, 2023
523d1f1
Added docs
matthewlipski Apr 12, 2023
6493392
Small update
matthewlipski Apr 12, 2023
6771d2e
Made selection undefined if nothing is selected
matthewlipski Apr 13, 2023
ba85c98
Added selection docs
matthewlipski Apr 13, 2023
a0835b9
Added styling docs
matthewlipski Apr 13, 2023
b71a46d
Added nesting & un-nesting docs
matthewlipski Apr 13, 2023
64c47ac
Small fix
matthewlipski Apr 13, 2023
22eea02
Added selection demo
matthewlipski Apr 13, 2023
3b00be6
Minor fixes
matthewlipski Apr 13, 2023
ef0abd9
fix: hyperlink creation menu losing focus on click (#168)
matthewlipski Apr 18, 2023
0812f31
Doc changes & cleanup
matthewlipski Apr 19, 2023
8d73ce3
Merge branch 'main' into custom-formattingtoolbar
matthewlipski Apr 19, 2023
40b3cb0
feat: customizable sidemenu (#143)
YousefED Apr 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/vanilla/src/ui/formattingToolbarFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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";
},
Expand Down
215 changes: 213 additions & 2 deletions packages/core/src/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -102,6 +108,10 @@ export class BlockNoteEditor {
return this._tiptapEditor.view.dom as HTMLDivElement;
}

public focus() {
this._tiptapEditor.view.focus();
}

constructor(options: Partial<BlockNoteEditorOptions> = {}) {
// apply defaults
options = {
Expand Down Expand Up @@ -229,15 +239,15 @@ export class BlockNoteEditor {

function traverseBlockArray(blockArray: Block[]): boolean {
for (const block of blockArray) {
if (callback(block) === false) {
if (!callback(block)) {
return false;
}

const children = reverse
? block.children.slice().reverse()
: block.children;

if (traverseBlockArray(children) === false) {
if (!traverseBlockArray(children)) {
return false;
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<ToggledStyle>([
"bold",
"italic",
"underline",
"strike",
"code",
]);
const colorStyles = new Set<ColorStyle>(["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<ToggledStyle>([
"bold",
"italic",
"underline",
"strike",
"code",
]);
const colorStyles = new Set<ColorStyle>(["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<ToggledStyle>([
"bold",
"italic",
"underline",
"strike",
"code",
]);
const colorStyles = new Set<ColorStyle>(["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.
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -100,6 +99,7 @@ export const getBlockNoteExtensions = (opts: {
if (opts.uiFactories.blockSideMenuFactory) {
ret.push(
DraggableBlocksExtension.configure({
editor: opts.editor,
blockSideMenuFactory: opts.uiFactories.blockSideMenuFactory,
})
);
Expand All @@ -108,6 +108,7 @@ export const getBlockNoteExtensions = (opts: {
if (opts.uiFactories.formattingToolbarFactory) {
ret.push(
FormattingToolbarExtension.configure({
editor: opts.editor,
formattingToolbarFactory: opts.uiFactories.formattingToolbarFactory,
})
);
Expand Down
20 changes: 10 additions & 10 deletions packages/core/src/api/nodeConversions/nodeConversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToggledStyles>([
const toggleStyles = new Set<ToggledStyle>([
"bold",
"italic",
"underline",
"strike",
"code",
]);
const colorStyles = new Set<ColorStyles>(["textColor", "backgroundColor"]);
const colorStyles = new Set<ColorStyle>(["textColor", "backgroundColor"]);

/**
* Convert a StyledText inline element to a
Expand All @@ -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 }));
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/extensions/Blocks/api/inlineContentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ export type Styles = {
backgroundColor?: string;
};

export type ToggledStyles = {
export type ToggledStyle = {
[K in keyof Styles]-?: Required<Styles>[K] extends true ? K : never;
}[keyof Styles];

export type ColorStyles = {
export type ColorStyle = {
[K in keyof Styles]-?: Required<Styles>[K] extends string ? K : never;
}[keyof Styles];

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/extensions/Blocks/api/selectionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Block } from "./blockTypes";

export type Selection = {
blocks: Block[];
};
Loading