Skip to content

Selection

Selection is a very common concept in structure editors. It's used for representing 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 CRDT-native data flow, which means the selection state is always derived from serializable data.

Selection Model

The selection model contains a list of atomic selections. Each selection represents a range of the content. For example, if you have a text block with the following content:

Hello

World

In the default PageEditor, it will be modeled as following block tree nodes:

Root Block
  Note Block
    Paragraph Block 1
    Paragraph Block 2

So if you select the text partially via mouse drag as following:

text-selection-example

The selection model will be:

ts
[
  {
    type: 'text',
    group: 'note',
    from: {
      path: ['root_id', 'note_id', 'paragraph_1_id'],
      index: 1,
      length: 5,
    },
    to: {
      path: ['root_id', 'note_id', 'paragraph_2_id'],
      index: 0,
      length: 4,
    },
  },
];

If you select the blocks via block level selection like this:

block-selection-example

The selection model will be:

ts
[
  {
    type: 'block',
    group: 'note',
    path: ['root_id', 'note_id', 'paragraph_1_id'],
  },
  {
    type: 'block',
    group: 'note',
    path: ['root_id', 'note_id', 'paragraph_2_id'],
  },
];

Types and Groups

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 indicates the scope of selection.

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 affine:note block. And you may also have a cell and row selection in a table block, and they can share the table group.

Update Selection State

You can get the selection manager from std.selection or host.selection. With the selection manager, you can read the selection model from value. And you can also write the selection model by set and update:

ts
const { selection } = host.std;

const current = selection.value;
const next = transformSelection(current);
selection.set(next);

// This can also be written as:
selection.update(current => transformSelection(current));

The set method will override all current selections.

You can also create a new selection by using selection.create method:

ts
const blockSelection = selection.create('block', { path: [0, 1, 2] });

If you want to pick some selections by type from the current selection model, you can reuse the pick and find methods to help:

ts
const textSelection: Selection = selection.pick('text');
const blockSelections: Selection[] = selection.find('block');

You can also clear all the selections by calling clear. If you just want to clear a certain type of selections, you can pass the type as the first argument of clear method:

ts
// 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.

ts
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.

ts
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.

ts
selection.slots.remoteChanged.on(nextSelectionMap => {
  for (const [userId, nextSelection] of nextSelectionMap) {
    renderRemoteSelectionToUI(nextSelection, userId);
  }
});

Create Custom Selection

You can create your own selection type by extending the BaseSelection interface.

ts
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:

ts
selection.register(MySelection);

Now you can use the MySelection in the selection model.

ts
const mySelection = selection.create('mySelection', {
  path: ['a', 'b', 'c'],
});