Skip to content

Commit 440a7e3

Browse files
committed
feat: selection stuff
1 parent ded7f69 commit 440a7e3

File tree

5 files changed

+406
-3
lines changed

5 files changed

+406
-3
lines changed

packages/core/src/api/nodeConversions/nodeToBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export function docToBlocks<
504504
S extends StyleSchema,
505505
>(
506506
doc: Node,
507-
schema: Schema,
507+
schema: Schema = getPmSchema(doc),
508508
blockSchema: BSchema = getBlockSchema(schema) as BSchema,
509509
inlineContentSchema: I = getInlineContentSchema(schema) as I,
510510
styleSchema: S = getStyleSchema(schema) as S,

packages/core/src/editor/managers/SelectionManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { Selection } from "../selectionTypes.js";
2323
import { TextCursorPosition } from "../cursorPositionTypes.js";
2424
import { BlockNoteEditor } from "../BlockNoteEditor.js";
25+
import { getSelectionLocation } from "../../locations/location.js";
2526

2627
export class SelectionManager<
2728
BSchema extends BlockSchema = DefaultBlockSchema,
@@ -40,6 +41,10 @@ export class SelectionManager<
4041
return this.editor.transact((tr) => getSelection(tr));
4142
}
4243

44+
public getSelectionLocation() {
45+
return this.editor.transact((tr) => getSelectionLocation(tr));
46+
}
47+
4348
/**
4449
* Gets a snapshot of the current selection. This contains all blocks (included nested blocks)
4550
* that the selection spans across.

packages/core/src/locations/location.test.ts

Lines changed: 275 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
resolvePointToPM,
77
resolveRangeToPM,
88
resolvePMToLocation,
9+
getBlocksAt,
10+
getSelectionLocation,
11+
setSelectionLocation,
912
} from "./location.js";
1013
import type { BlockId, Point, Range } from "./types.js";
1114
import { Node } from "prosemirror-model";
@@ -229,7 +232,7 @@ describe("Location Resolution", () => {
229232

230233
expect(() => {
231234
resolvePointToPM(doc, point);
232-
}).toThrow("Offset 100 exceeds block length");
235+
}).toThrow("Invalid offset: 100 exceeds block length 13");
233236
});
234237

235238
it("should handle different block types with points", () => {
@@ -587,7 +590,7 @@ describe("Location Resolution", () => {
587590

588591
expect(() => {
589592
resolvePointToPM(nestedDoc, shortChild);
590-
}).toThrow("Offset 100 exceeds block length");
593+
}).toThrow("Invalid offset: 100 exceeds block length 20");
591594
});
592595
});
593596

@@ -1548,3 +1551,273 @@ describe("Location Resolution", () => {
15481551
});
15491552
});
15501553
});
1554+
1555+
describe("getBlocksAt", () => {
1556+
let editor: BlockNoteEditor;
1557+
let doc: Node;
1558+
1559+
beforeEach(() => {
1560+
editor = BlockNoteEditor.create({
1561+
initialContent: [
1562+
{
1563+
id: "block1",
1564+
type: "paragraph",
1565+
content: "Hello World",
1566+
},
1567+
{
1568+
id: "block2",
1569+
type: "heading",
1570+
content: "This is a heading",
1571+
children: [
1572+
{
1573+
id: "block2-child-1",
1574+
type: "paragraph",
1575+
content: "This is a child paragraph",
1576+
},
1577+
{
1578+
id: "block2-child-2",
1579+
type: "paragraph",
1580+
content: "This is another child paragraph",
1581+
},
1582+
],
1583+
},
1584+
{
1585+
id: "block3",
1586+
type: "paragraph",
1587+
content: "Another paragraph with more text content",
1588+
},
1589+
{
1590+
id: "block4",
1591+
type: "bulletListItem",
1592+
content: "List item one",
1593+
},
1594+
{
1595+
id: "block5",
1596+
type: "bulletListItem",
1597+
content: "List item two",
1598+
},
1599+
],
1600+
});
1601+
1602+
// Get the ProseMirror document
1603+
doc = editor.prosemirrorState.doc;
1604+
});
1605+
1606+
afterEach(() => {
1607+
editor._tiptapEditor.destroy();
1608+
});
1609+
1610+
it("should return the blocks at the given location", () => {
1611+
const blocks = getBlocksAt(doc, { id: "block1", offset: 5 });
1612+
expect(blocks).toMatchInlineSnapshot(`
1613+
[
1614+
{
1615+
"children": [],
1616+
"content": [
1617+
{
1618+
"styles": {},
1619+
"text": "Hello World",
1620+
"type": "text",
1621+
},
1622+
],
1623+
"id": "block1",
1624+
"props": {
1625+
"backgroundColor": "default",
1626+
"textAlignment": "left",
1627+
"textColor": "default",
1628+
},
1629+
"type": "paragraph",
1630+
},
1631+
]
1632+
`);
1633+
});
1634+
1635+
it("should return the blocks at the given id object", () => {
1636+
const blocks = getBlocksAt(doc, { id: "block1" });
1637+
expect(blocks).toMatchInlineSnapshot(`
1638+
[
1639+
{
1640+
"children": [],
1641+
"content": [
1642+
{
1643+
"styles": {},
1644+
"text": "Hello World",
1645+
"type": "text",
1646+
},
1647+
],
1648+
"id": "block1",
1649+
"props": {
1650+
"backgroundColor": "default",
1651+
"textAlignment": "left",
1652+
"textColor": "default",
1653+
},
1654+
"type": "paragraph",
1655+
},
1656+
]
1657+
`);
1658+
});
1659+
1660+
it("should return the blocks at the given id", () => {
1661+
const blocks = getBlocksAt(doc, "block1");
1662+
expect(blocks).toMatchInlineSnapshot(`
1663+
[
1664+
{
1665+
"children": [],
1666+
"content": [
1667+
{
1668+
"styles": {},
1669+
"text": "Hello World",
1670+
"type": "text",
1671+
},
1672+
],
1673+
"id": "block1",
1674+
"props": {
1675+
"backgroundColor": "default",
1676+
"textAlignment": "left",
1677+
"textColor": "default",
1678+
},
1679+
"type": "paragraph",
1680+
},
1681+
]
1682+
`);
1683+
});
1684+
1685+
it("should return the blocks at the given location with children", () => {
1686+
const blocks = getBlocksAt(
1687+
doc,
1688+
{
1689+
anchor: { id: "block2", offset: -1 },
1690+
head: { id: "block3", offset: -1 },
1691+
},
1692+
{ includeChildren: true },
1693+
);
1694+
expect(blocks.map((block) => block.id)).toEqual([
1695+
"block2",
1696+
"block2-child-1",
1697+
"block2-child-2",
1698+
"block3",
1699+
]);
1700+
});
1701+
1702+
it("should exclude children when includeChildren is false", () => {
1703+
const blocks = getBlocksAt(
1704+
doc,
1705+
{
1706+
anchor: { id: "block2", offset: -1 },
1707+
head: { id: "block3", offset: -1 },
1708+
},
1709+
{ includeChildren: false },
1710+
);
1711+
expect(blocks.map((block) => block.id)).toEqual(["block2", "block3"]);
1712+
});
1713+
1714+
it("should return the blocks at the given location even if the location's head is before the anchor", () => {
1715+
const blocks = getBlocksAt(
1716+
doc,
1717+
{
1718+
anchor: { id: "block3", offset: -1 },
1719+
head: { id: "block2", offset: -1 },
1720+
},
1721+
{ includeChildren: true },
1722+
);
1723+
expect(blocks.map((block) => block.id)).toEqual([
1724+
"block2",
1725+
"block2-child-1",
1726+
"block2-child-2",
1727+
"block3",
1728+
]);
1729+
});
1730+
1731+
it("should return the blocks at the given location even if the location's head is before the anchor and includeChildren is false", () => {
1732+
const blocks = getBlocksAt(
1733+
doc,
1734+
{
1735+
anchor: { id: "block3", offset: -1 },
1736+
head: { id: "block2", offset: -1 },
1737+
},
1738+
{ includeChildren: false },
1739+
);
1740+
expect(blocks.map((block) => block.id)).toEqual(["block2", "block3"]);
1741+
});
1742+
});
1743+
1744+
describe("getSelectionLocation", () => {
1745+
let editor: BlockNoteEditor;
1746+
let doc: Node;
1747+
1748+
beforeEach(() => {
1749+
editor = BlockNoteEditor.create({
1750+
initialContent: [
1751+
{
1752+
id: "block1",
1753+
type: "paragraph",
1754+
content: "Hello World",
1755+
},
1756+
],
1757+
});
1758+
1759+
doc = editor.prosemirrorState.doc;
1760+
});
1761+
1762+
afterEach(() => {
1763+
editor._tiptapEditor.destroy();
1764+
});
1765+
1766+
it("should return undefined if the selection is empty", () => {
1767+
const blocks = getSelectionLocation(editor.prosemirrorState.tr);
1768+
expect(blocks).toMatchInlineSnapshot(`undefined`);
1769+
});
1770+
1771+
it("should return the blocks at the given location", () => {
1772+
editor.exec((state, dispatch) => {
1773+
if (dispatch) {
1774+
const tr = state.tr;
1775+
setSelectionLocation(tr, { id: "block1", offset: 5 });
1776+
dispatch(tr);
1777+
}
1778+
return true;
1779+
});
1780+
expect(editor.prosemirrorState.selection.toJSON()).toMatchInlineSnapshot(`
1781+
{
1782+
"anchor": 8,
1783+
"head": 8,
1784+
"type": "text",
1785+
}
1786+
`);
1787+
const blocks = getSelectionLocation(editor.prosemirrorState.tr);
1788+
expect(blocks).toMatchInlineSnapshot(`
1789+
{
1790+
"blocks": [
1791+
{
1792+
"children": [],
1793+
"content": [
1794+
{
1795+
"styles": {},
1796+
"text": "Hello World",
1797+
"type": "text",
1798+
},
1799+
],
1800+
"id": "block1",
1801+
"props": {
1802+
"backgroundColor": "default",
1803+
"textAlignment": "left",
1804+
"textColor": "default",
1805+
},
1806+
"type": "paragraph",
1807+
},
1808+
],
1809+
"isPointingToBlock": false,
1810+
"range": {
1811+
"anchor": {
1812+
"id": "block1",
1813+
"offset": 5,
1814+
},
1815+
"head": {
1816+
"id": "block1",
1817+
"offset": 5,
1818+
},
1819+
},
1820+
}
1821+
`);
1822+
});
1823+
});

0 commit comments

Comments
 (0)