Skip to content

Commit ae2d35d

Browse files
committed
Merge branch 'main' into new-releases
2 parents 31e3c53 + 572ee42 commit ae2d35d

File tree

12 files changed

+324
-46
lines changed

12 files changed

+324
-46
lines changed

packages/core/src/extensions/Blocks/api/block.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,39 @@ export function propsToAttributes<
3838
// Props are displayed in kebab-case as HTML attributes. If a prop's
3939
// value is the same as its default, we don't display an HTML
4040
// attribute for it.
41-
parseHTML: (element) => element.getAttribute(camelToDataKebab(name)),
41+
parseHTML: (element) => {
42+
const value = element.getAttribute(camelToDataKebab(name));
43+
44+
if (value === null) {
45+
return null;
46+
}
47+
48+
if (typeof spec.default === "boolean") {
49+
if (value === "true") {
50+
return true;
51+
}
52+
53+
if (value === "false") {
54+
return false;
55+
}
56+
57+
return null;
58+
}
59+
60+
if (typeof spec.default === "number") {
61+
const asNumber = parseFloat(value);
62+
const isNumeric =
63+
!Number.isNaN(asNumber) && Number.isFinite(asNumber);
64+
65+
if (isNumeric) {
66+
return asNumber;
67+
}
68+
69+
return null;
70+
}
71+
72+
return value;
73+
},
4274
renderHTML: (attributes) =>
4375
attributes[name] !== spec.default
4476
? {

packages/core/src/extensions/Blocks/nodes/BlockContainer.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,51 @@ export const BlockContainer = Node.create<{
485485
}),
486486
]);
487487

488+
const handleDelete = () =>
489+
this.editor.commands.first(({ commands }) => [
490+
// Deletes the selection if it's not empty.
491+
() => commands.deleteSelection(),
492+
// Merges block with the next one (at the same nesting level or lower),
493+
// if one exists, the block has no children, and the selection is at the
494+
// end of the block.
495+
() =>
496+
commands.command(({ state }) => {
497+
const { node, contentNode, depth, endPos } = getBlockInfoFromPos(
498+
state.doc,
499+
state.selection.from
500+
)!;
501+
502+
const blockAtDocEnd = false;
503+
const selectionAtBlockEnd =
504+
state.selection.$anchor.parentOffset ===
505+
contentNode.firstChild!.nodeSize;
506+
const selectionEmpty =
507+
state.selection.anchor === state.selection.head;
508+
const hasChildBlocks = node.childCount === 2;
509+
510+
if (
511+
!blockAtDocEnd &&
512+
selectionAtBlockEnd &&
513+
selectionEmpty &&
514+
!hasChildBlocks
515+
) {
516+
let oldDepth = depth;
517+
let newPos = endPos + 2;
518+
let newDepth = state.doc.resolve(newPos).depth;
519+
520+
while (newDepth < oldDepth) {
521+
oldDepth = newDepth;
522+
newPos += 2;
523+
newDepth = state.doc.resolve(newPos).depth;
524+
}
525+
526+
return commands.BNMergeBlocks(newPos - 1);
527+
}
528+
529+
return false;
530+
}),
531+
]);
532+
488533
const handleEnter = () =>
489534
this.editor.commands.first(({ commands }) => [
490535
// Removes a level of nesting if the block is empty & indented, while the selection is also empty & at the start
@@ -552,12 +597,14 @@ export const BlockContainer = Node.create<{
552597
state.selection.from
553598
)!;
554599

600+
const selectionAtBlockStart =
601+
state.selection.$anchor.parentOffset === 0;
555602
const blockEmpty = node.textContent.length === 0;
556603

557604
if (!blockEmpty) {
558605
chain()
559606
.deleteSelection()
560-
.BNSplitBlock(state.selection.from, false)
607+
.BNSplitBlock(state.selection.from, selectionAtBlockStart)
561608
.run();
562609

563610
return true;
@@ -569,6 +616,7 @@ export const BlockContainer = Node.create<{
569616

570617
return {
571618
Backspace: handleBackspace,
619+
Delete: handleDelete,
572620
Enter: handleEnter,
573621
// Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the
574622
// editor since the browser will try to use tab for keyboard navigation.

packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts

+4
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ export const setupSuggestionsMenu = <
346346

347347
// Selects an item and closes the menu.
348348
if (event.key === "Enter") {
349+
if (items.length === 0) {
350+
return true;
351+
}
352+
349353
deactivate(view);
350354
editor._tiptapEditor
351355
.chain()

packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { ToolbarDropdown } from "../../../SharedComponents/Toolbar/components/ToolbarDropdown";
1919
import { ToolbarDropdownItemProps } from "../../../SharedComponents/Toolbar/components/ToolbarDropdownItem";
2020
import { useEditorChange } from "../../../hooks/useEditorChange";
21+
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks";
2122

2223
export type BlockTypeDropdownItem = {
2324
name: string;
@@ -42,7 +43,7 @@ export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [
4243
isSelected: (block) =>
4344
block.type === "heading" &&
4445
"level" in block.props &&
45-
block.props.level === "1",
46+
block.props.level === 1,
4647
},
4748
{
4849
name: "Heading 2",
@@ -52,7 +53,7 @@ export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [
5253
isSelected: (block) =>
5354
block.type === "heading" &&
5455
"level" in block.props &&
55-
block.props.level === "2",
56+
block.props.level === 2,
5657
},
5758
{
5859
name: "Heading 3",
@@ -62,7 +63,7 @@ export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [
6263
isSelected: (block) =>
6364
block.type === "heading" &&
6465
"level" in block.props &&
65-
block.props.level === "3",
66+
block.props.level === 3,
6667
},
6768
{
6869
name: "Bullet List",
@@ -82,6 +83,8 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
8283
editor: BlockNoteEditor<BSchema>;
8384
items?: BlockTypeDropdownItem[];
8485
}) => {
86+
const selectedBlocks = useSelectedBlocks(props.editor);
87+
8588
const [block, setBlock] = useState(
8689
props.editor.getTextCursorPosition().block
8790
);
@@ -123,10 +126,13 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
123126
const fullItems: ToolbarDropdownItemProps[] = useMemo(() => {
124127
const onClick = (item: BlockTypeDropdownItem) => {
125128
props.editor.focus();
126-
props.editor.updateBlock(block, {
127-
type: item.type,
128-
props: item.props,
129-
} as PartialBlock<BlockSchema>);
129+
130+
for (const block of selectedBlocks) {
131+
props.editor.updateBlock(block, {
132+
type: item.type,
133+
props: item.props,
134+
} as PartialBlock<BlockSchema>);
135+
}
130136
};
131137

132138
return filteredItems.map((item) => ({
@@ -135,7 +141,7 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
135141
onClick: () => onClick(item),
136142
isSelected: item.isSelected(block as Block<BlockSchema>),
137143
}));
138-
}, [block, filteredItems, props.editor]);
144+
}, [block, filteredItems, props.editor, selectedBlocks]);
139145

140146
useEditorChange(props.editor, () => {
141147
setBlock(props.editor.getTextCursorPosition().block);

packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx

+32-32
Original file line numberDiff line numberDiff line change
@@ -59,38 +59,38 @@ export const BlockColorsButton = <BSchema extends BlockSchema>(
5959
</div>
6060
</Menu.Target>
6161
<div ref={ref}>
62-
<Menu.Dropdown
63-
onMouseLeave={startMenuCloseTimer}
64-
onMouseOver={stopMenuCloseTimer}
65-
style={{ marginLeft: "5px" }}>
66-
<ColorPicker
67-
iconSize={18}
68-
text={
69-
"textColor" in props.block.props &&
70-
typeof props.block.props.textColor === "string"
71-
? {
72-
color: props.block.props.textColor,
73-
setColor: (color) =>
74-
props.editor.updateBlock(props.block, {
75-
props: { textColor: color },
76-
} as PartialBlock<BSchema>),
77-
}
78-
: undefined
79-
}
80-
background={
81-
"backgroundColor" in props.block.props &&
82-
typeof props.block.props.backgroundColor === "string"
83-
? {
84-
color: props.block.props.backgroundColor,
85-
setColor: (color) =>
86-
props.editor.updateBlock(props.block, {
87-
props: { backgroundColor: color },
88-
} as PartialBlock<BSchema>),
89-
}
90-
: undefined
91-
}
92-
/>
93-
</Menu.Dropdown>
62+
<Menu.Dropdown
63+
onMouseLeave={startMenuCloseTimer}
64+
onMouseOver={stopMenuCloseTimer}
65+
style={{ marginLeft: "5px" }}>
66+
<ColorPicker
67+
iconSize={18}
68+
text={
69+
"textColor" in props.block.props &&
70+
typeof props.block.props.textColor === "string"
71+
? {
72+
color: props.block.props.textColor,
73+
setColor: (color) =>
74+
props.editor.updateBlock(props.block, {
75+
props: { textColor: color },
76+
} as PartialBlock<BSchema>),
77+
}
78+
: undefined
79+
}
80+
background={
81+
"backgroundColor" in props.block.props &&
82+
typeof props.block.props.backgroundColor === "string"
83+
? {
84+
color: props.block.props.backgroundColor,
85+
setColor: (color) =>
86+
props.editor.updateBlock(props.block, {
87+
props: { backgroundColor: color },
88+
} as PartialBlock<BSchema>),
89+
}
90+
: undefined
91+
}
92+
/>
93+
</Menu.Dropdown>
9494
</div>
9595
</Menu>
9696
</DragHandleMenuItem>

packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenuItem.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Menu } from "@mantine/core";
1+
import { Menu, MenuItemProps } from "@mantine/core";
22
import { PolymorphicComponentProps } from "@mantine/utils";
33

44
export const DragHandleMenuItem = (
5-
props: PolymorphicComponentProps<"button">
5+
props: PolymorphicComponentProps<"button"> & MenuItemProps
66
) => {
77
const { children, ...remainingProps } = props;
88
return <Menu.Item {...remainingProps}>{children}</Menu.Item>;

packages/website/docs/data.ts

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export const sponsors: Sponsors[] = [
2020
imgDark: "/img/sponsors/nlnet-dark.svg",
2121
imgLight: "/img/sponsors/nlnet.svg",
2222
},
23+
{
24+
name: "Twenty",
25+
imgDark: "/img/sponsors/twenty-dark.png",
26+
imgLight: "/img/sponsors/twenty.png",
27+
},
2328
];
2429

2530
export interface FeaturesCardData {

packages/website/docs/docs/side-menu.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ type SideMenuButtonProps = {
177177
}
178178
export const SideMenuButton = (props: SideMenuButtonProps) => ...;
179179

180-
// Takes same props as `button` elements, e.g. onClick.
181-
export const DragHandleMenuItem = (props) => ...;
180+
// Contains all props that a regular button element would take, as well as all props from the Mantine `Menu.Item` component.
181+
type DragHandleMenuItemProps = PolymorphicComponentProps<"button"> & MenuItemProps
182+
export const DragHandleMenuItem = (props: DragHandleMenuItemProps) => ...;
182183
```
Loading
Loading

tests/end-to-end/copypaste/copypaste.test.ts

+56
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
insertNestedListItems,
1010
insertParagraph,
1111
} from "../../utils/copypaste";
12+
import { executeSlashCommand } from "../../utils/slashmenu";
1213

1314
test.describe.configure({ mode: "serial" });
1415

@@ -135,4 +136,59 @@ test.describe("Check Copy/Paste Functionality", () => {
135136

136137
await compareDocToSnapshot(page, "nestedOrderedLists.json");
137138
});
139+
140+
test("Images should keep props", async ({ page, browserName }) => {
141+
test.skip(
142+
browserName === "firefox" || browserName === "webkit",
143+
"Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33."
144+
);
145+
146+
await focusOnEditor(page);
147+
await page.keyboard.type("paragraph");
148+
149+
const IMAGE_EMBED_URL =
150+
"https://www.pulsecarshalton.co.uk/wp-content/uploads/2016/08/jk-placeholder-image.jpg";
151+
await executeSlashCommand(page, "image");
152+
153+
await page.click(`[data-test="embed-tab"]`);
154+
await page.click(`[data-test="embed-input"]`);
155+
await page.keyboard.type(IMAGE_EMBED_URL);
156+
await page.click(`[data-test="embed-input-button"]`);
157+
await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);
158+
159+
await page.click(`img`);
160+
161+
await page.waitForSelector(`[class*="resizeHandle"][style*="right"]`);
162+
const resizeHandle = page.locator(
163+
`[class*="resizeHandle"][style*="right"]`
164+
);
165+
const resizeHandleBoundingBox = await resizeHandle.boundingBox();
166+
await page.mouse.move(
167+
resizeHandleBoundingBox.x + resizeHandleBoundingBox.width / 2,
168+
resizeHandleBoundingBox.y + resizeHandleBoundingBox.height / 2,
169+
{
170+
steps: 5,
171+
}
172+
);
173+
await page.mouse.down();
174+
175+
await page.mouse.move(
176+
resizeHandleBoundingBox.x + resizeHandleBoundingBox.width / 2 - 50,
177+
resizeHandleBoundingBox.y + resizeHandleBoundingBox.height / 2,
178+
{
179+
steps: 5,
180+
}
181+
);
182+
183+
await page.mouse.up();
184+
185+
await page.click(`img`);
186+
await page.keyboard.press("ArrowDown");
187+
await page.pause();
188+
189+
await copyPasteAll(page);
190+
await page.pause();
191+
192+
await compareDocToSnapshot(page, "images.json");
193+
});
138194
});

0 commit comments

Comments
 (0)