Selection API ​
Selection is a very common concept in structural editors. It's used to represent the current cursor position or the current selected blocks.
In BlockSuite, we use a data driven approach to represent the selection. It also follows the unidirectional data flow, which means the selection is always derived from the data.
Selection Model ​
The selection model contains a list of selections. Each selection is a range of the content. For example, if you have a text block with the following content:
Hello
World
In BlockSuite, it will be represented as following:
|-- Root Block (id: 0)
|-- Note Block (id: 1)
|-- Text Block (id: 2)
|-- Text Block (id: 3)
|-- Root Block (id: 0)
|-- Note Block (id: 1)
|-- Text Block (id: 2)
|-- Text Block (id: 3)
And if you select the text partially via mouse drag as following:
The selection model will be:
[
{
type: 'text',
group: 'note',
from: {
path: [0, 1, 2],
index: 1,
length: 5,
},
to: {
path: [0, 1, 3],
index: 0,
length: 4,
},
},
];
[
{
type: 'text',
group: 'note',
from: {
path: [0, 1, 2],
index: 1,
length: 5,
},
to: {
path: [0, 1, 3],
index: 0,
length: 4,
},
},
];
If you select the blocks via block selection like this:
The selection model will be:
[
{
type: 'block',
group: 'note',
path: [0, 1, 2],
},
{
type: 'block',
group: 'note',
path: [0, 1, 3],
},
];
[
{
type: 'block',
group: 'note',
path: [0, 1, 2],
},
{
type: 'block',
group: 'note',
path: [0, 1, 3],
},
];
Type and Group ​
Selection model has two important properties: type
and group
.
The type
of a selection means which kind of selection it is. And the group
of a selection means the selection's scope.
Some types of selections can share the same group because they have the same scope. For example, the text
selection and the block
selection can share the note
group because they are both in the note
block. And you may also have a cell
and row
selection in a table
block, and they can share the table
group.
Read and Write Selection ​
You can get the selection manager from std.selection
. With the manager, you can read the selection model from value
. And you can also write the selection model by set
and update
.
const { selection } = std;
const current = selection.value;
const next = yourLogic(current);
selection.set(next);
// This can also be written as:
selection.update(current => yourLogic(current));
const { selection } = std;
const current = selection.value;
const next = yourLogic(current);
selection.set(next);
// This can also be written as:
selection.update(current => yourLogic(current));
The set
method will override all current selections.
You can create a new selection by using getInstance
method.
const { selection } = std;
const blockSelection = selection.getInstance('block', { path: [0, 1, 2] });
const { selection } = std;
const blockSelection = selection.getInstance('block', { path: [0, 1, 2] });
What if you want to pick some selections by type
from the current selection model? We provide pick
and find
methods to help you.
const { selection } = std;
const textSelection: Selection = selection.pick('text');
const blockSelections: Selection[] = selection.find('block');
const { selection } = std;
const textSelection: Selection = selection.pick('text');
const blockSelections: Selection[] = selection.find('block');
You can clear all the selections by call clear
. If you just want to clear a type of selections, you can pass the type as the first argument of clear
method.
const { selection } = std;
// clear all selections
selection.clear();
// clear text selection
selection.clear('text');
const { selection } = std;
// clear all selections
selection.clear();
// clear text selection
selection.clear('text');
And we also provide a setGroup
method to override the selections in a specific group. Of course, we also provide a getGroup
method.
const { selection } = std;
const noteSelections = selection.getGroup('note');
const nextNoteSelections = yourLogic(noteSelections);
selection.setGroup('note', nextNoteSelections);
const { selection } = std;
const noteSelections = selection.getGroup('note');
const nextNoteSelections = yourLogic(noteSelections);
selection.setGroup('note', nextNoteSelections);
Subscribe to Selection Changes ​
You can subscribe to the selection changes by using changed
slot.
const { selection } = std;
selection.slots.changed.on(nextSelection => {
renderSelectionToUI(nextSelection);
});
const { selection } = std;
selection.slots.changed.on(nextSelection => {
renderSelectionToUI(nextSelection);
});
You can also subscribe to the remote selection changes by using remoteChanged
slot. This is useful when you want to display the selection of other users.
const { selection } = std;
selection.slots.remoteChanged.on(nextSelectionMap => {
for (const [userId, nextSelection] of nextSelectionMap) {
renderRemoteSelectionToUI(nextSelection, userId);
}
});
const { selection } = std;
selection.slots.remoteChanged.on(nextSelectionMap => {
for (const [userId, nextSelection] of nextSelectionMap) {
renderRemoteSelectionToUI(nextSelection, userId);
}
});
Create a Custom Selection ​
You can create your own selection type by extending the BaseSelection
interface.
import { BaseSelection, PathFinder } from '@blocksuite/block-std';
import z from 'zod';
const MySelectionSchema = z.object({
path: z.array(z.string()),
});
export class MySelection extends BaseSelection {
static override type = 'mySelection';
static override group = 'note';
override equals(other: BaseSelection): boolean {
if (other instanceof MySelection) {
return PathFinder.equals(this.path, other.path);
}
return false;
}
override toJSON(): Record<string, unknown> {
return {
type: this.type,
path: this.path,
};
}
static override fromJSON(json: Record<string, unknown>): ImageSelection {
MySelectionSchema.parse(json);
return new MySelection({
path: json.path as string[],
});
}
}
declare global {
namespace BlockSuite {
interface Selection {
mySelection: typeof MySelection;
}
}
}
import { BaseSelection, PathFinder } from '@blocksuite/block-std';
import z from 'zod';
const MySelectionSchema = z.object({
path: z.array(z.string()),
});
export class MySelection extends BaseSelection {
static override type = 'mySelection';
static override group = 'note';
override equals(other: BaseSelection): boolean {
if (other instanceof MySelection) {
return PathFinder.equals(this.path, other.path);
}
return false;
}
override toJSON(): Record<string, unknown> {
return {
type: this.type,
path: this.path,
};
}
static override fromJSON(json: Record<string, unknown>): ImageSelection {
MySelectionSchema.parse(json);
return new MySelection({
path: json.path as string[],
});
}
}
declare global {
namespace BlockSuite {
interface Selection {
mySelection: typeof MySelection;
}
}
}
After that, you need to register the selection to selection manager:
const { selection } = std;
std.selection.register(MySelection);
const { selection } = std;
std.selection.register(MySelection);
Now you can use the MySelection
in the selection model.
const mySelection = std.selection.getInstance('mySelection', {
path: [0, 1, 2],
});
const mySelection = std.selection.getInstance('mySelection', {
path: [0, 1, 2],
});