Working with Block Tree
In previous examples, we demonstrated how a page
collaborates with an editor
. In this document, we will introduce the basic structure of the block tree within the page
and the common methods for controlling it in an editor environment.
Block Tree Basics
In BlockSuite, each page
object manages an independent block tree composed of various types of blocks. These blocks can be defined through the BlockSchema
, which specifies their fields and permissible nesting relationships among different block types. Each block type has a unique block.flavour
, following a namespace:name
naming structure. Since the preset editors in BlockSuite are derived from the AFFiNE project, the default editable blocks use the affine
prefix.
To manipulate blocks, you can utilize several primary APIs under page
:
Here is an example demonstrating the manipulation of the block tree through these APIs:
// The first block will be added as root
const rootId = page.addBlock('affine:page');
// Insert second block as a child of the root with empty props
const noteId = page.addBlock('affine:note', {}, rootId);
// You can also provide an optional `parentIndex`
const paragraphId = page.addBlock('affine:paragraph', {}, noteId, 0);
const modelA = page.root!.children[0].children[0];
const modelB = page.getBlockById(paragraphId);
console.log(modelA === modelB); // true
// Update the paragraph type to 'h1'
page.updateBlock(modelA, { type: 'h1' });
page.deleteBlock(modelA);
This example creates a subset of the block tree hierarchy defaultly used in @blocksuite/presets
, illustrated as follows:
INFO
The block tree hierarchy is specific to the preset editors. At the framework level, @blocksuite/store
does NOT treat the "first-party" affine:*
blocks with any special way. Feel free to add blocks from different namespaces for the block tree!
All block operations on page
are automatically recorded and can be reversed using page.undo()
and page.redo()
. By default, operations within a certain period are automatically merged into a single record. However, you can explicitly add a history record during operations by inserting page.captureSync()
between block operations:
const rootId = page.addBlock('affine:page');
const noteId = page.addBlock('affine:note', {}, rootId);
// Capture a history record now
page.captureSync();
// ...
This is particularly useful when adding multiple blocks at once but wishing to undo them individually.
Block Tree in Editor
To understand the common operations on the block tree in an editor environment, it's helpful to grasp the basic design of the editor. This can start with the following code snippet:
const { host } = editor;
const { spec, selection, command } = host.std;
Firstly, let's explain the newly introduced host
and std
, which are determined by the framework-agnostic architecture of BlockSuite:
- In BlockSuite, the
editor
itself is usually quite lightweight, serving primarily to provide access to thehost
. The actual editable blocks are registered oneditor.host
- also known as theEditorHost
component, which is a container for mounting block UI components. BlockSuite by default offers a host based on the lit framework. As long as there is a correspondinghost
implementation, you can use the component model of frameworks like react or vue to implement your BlockSuite editors. - Regardless of the framework used to implement
EditorHost
, they can access the same headless standard library designed for editable blocks throughhost.std
. For example,std.spec
contains all the registeredBlockSpec
s.
TIP
We usually access host.spec
instead of host.std.spec
to simplify the code.
As the runtime for the block tree, this is the mental model inside the editor
:
Selecting Blocks
TODO
Customizing Blocks
TODO