/blocksuite

💠 BlockSuite provides building blocks for collaborative applications.

Primary LanguageTypeScriptMozilla Public License 2.0MPL-2.0

BlockSuite

Checks Status Issues Closed NPM Latest Release NPM Nightly Release

💠 BlockSuite is the open-source editor project behind AFFiNE. It provides an out-of-the-box block-based editor built on top of a framework designed for general-purpose collaborative applications. This monorepo maintains both the editor and the underlying framework.

👉 Try BlockSuite-based AFFiNE online

Introduction

BlockSuite works very differently than traditional rich text frameworks:

  • For the data model, BlockSuite does not implement the event sourcing pattern but instead provides a CRDT-based block tree based directly on Yjs, supporting zero-cost time travel and real-time collaboration out of the box. Its data persistence layer is also designed to be local-first.
  • For rich text editing, multiple different nodes in the BlockSuite block tree can be connected to different rich text editing components, thus modeling rich text content as multiple UI components instead of a single UI container, eliminating the use of the dangerous monolith contenteditale.
  • For the rendering layer, BlockSuite does not assume that content can only be rendered through the DOM. It not only implements a basic document editing UI based on Web Components, but also develops a hybrid canvas-based renderer for parts of the whiteboard content. Both renderers can coexist on the same page and are updated from the same store.

BlockSuite is not intended to be yet another plugin-based rich text editing framework. Instead, it encourages building various collaborative applications directly through whatever UI framework you're comfortable with. To this end, we will try to open-source more foundational modules as reusable packages for this in the BlockSuite project.

Although BlockSuite is still in its early stages, you can already use the @blocksuite/editor package, the collaborative editor used in AFFiNE Alpha. Note that this editor is also a web component and is completely framework-independent!

Current Status (@blocksuite/editor)

⚠️ This project is under heavy development and is in a stage of rapid evolution. Stay tuned!

  • Basic text editing
    • ✅ Paragraph with inline style
    • ✅ Nested list
    • ✅ Code block
    • ✅ Markdown shortcuts
  • Block-level editing
    • ✅ Inline text format bar
    • ⚛️ Block-level selection
    • ⚛️ Block drag handle
    • ⚛️ Block hub
    • ⚛️ Inline slash menu
  • Rich-content
    • ⚛️ Image block
    • 🚧 Database block
    • 📌 Third-party embedded block
  • Whiteboard (edgeless mode)
    • ✅ Zooming and panning
    • ⚛️ Frame block
    • ⚛️ Shape element
    • 🚧 Handwriting element
    • 📌 Grouping
  • Playground
    • ✅ Multiplayer collaboration
    • ✅ Local data persistence
    • ✅ E2E test suite
  • Developer experience
    • ✅ Block tree update API
    • ✅ Zero cost time travel (undo/redo)
    • ✅ Reusable NPM package
    • ⚛️ React hooks integration
    • 📌 Dynamic block registration

Icons above correspond to the following meanings:

  • ✅ - Beta
  • ⚛️ - Alpha
  • 🚧 - Developing
  • 📌 - Planned

Resources

Getting Started

The @blocksuite/editor package contains the editor built into AFFiNE. Its nightly versions are released daily based on the master branch, and they are always tested on CI. This means that the nightly versions can already be used in real-world projects like AFFiNE at any time:

pnpm i @blocksuite/editor@nightly

If you want to easily reuse most of the rich-text editing features, you can use the SimpleAffineEditor web component directly (code example here):

import { SimpleAffineEditor } from '@blocksuite/editor';
import '@blocksuite/editor/src/themes/affine.css';

const editor = new SimpleAffineEditor();
document.body.appendChild(editor);

Or equivalently, you can also use the declarative style:

<body>
  <simple-affine-editor></simple-affine-editor>
  <script type="module">
    import '@blocksuite/editor';
    import '@blocksuite/editor/src/themes/affine.css';
  </script>
</body>

👉 Try SimpleAffineEditor online

However, the SimpleAffineEditor here is just a thin wrapper with dozens of lines that doesn't enable the opt-in collaboration and data persistence features. If you are going to support more complicated real-world use cases (e.g., with customized block models and configured data sources), this will involve the use of these three following core packages:

  • The packages/store package is a data store built for general-purpose state management.
  • The packages/blocks package holds the default BlockSuite editable blocks.
  • The packages/editor package ships a complete BlockSuite-based editor.
pnpm i \
  @blocksuite/store@nightly \
  @blocksuite/blocks@nightly \
  @blocksuite/editor@nightly

And here is a minimal collaboration-ready editor showing how these underlying BlockSuite packages are composed together:

🚧 Here we will work with the concepts of Workspace, Page, Block and Signal. These are the primitives for building a block-based collaborative application. We are preparing a comprehensive documentation about their usage!

import '@blocksuite/blocks';
// A workspace can hold multiple pages, and a page can hold multiple blocks.
import { Workspace, Page } from '@blocksuite/store';
import { BlockSchema } from '@blocksuite/blocks/models';
import { EditorContainer } from '@blocksuite/editor';

/**
 * Manually create the initial page structure.
 * In collaboration mode or on page refresh with local persistence,
 * the page data will be automatically loaded from store providers.
 * In these cases, this function should not be called.
 */
function createInitialPage(workspace: Workspace) {
  // Events are being emitted using signals.
  workspace.signals.pageAdded.once(id => {
    const page = workspace.getPage(id) as Page;

    // Block types are defined and registered in BlockSchema.
    const pageBlockId = page.addBlock({ flavour: 'affine:page' });
    const frameId = page.addBlock({ flavour: 'affine:frame' }, pageBlockId);
    page.addBlock({ flavour: 'affine:paragraph' }, frameId);
  });

  // Create a new page. This will trigger the signal above.
  workspace.createPage('page0');
}

// Subscribe for page update and create editor on page added.
function initEditorOnPageAdded(workspace: Workspace) {
  workspace.signals.pageAdded.once(pageId => {
    const page = workspace.getPage(pageId) as Page;
    const editor = new EditorContainer();
    editor.page = page;
    document.body.appendChild(editor);
  });
}

function main() {
  // Initialize the store.
  const workspace = new Workspace({}).register(BlockSchema);

  // Start waiting for the first page...
  initEditorOnPageAdded(workspace);

  // Suppose we are the first one to create the page.
  createInitialPage(workspace);
}

main();

For React developers, check out the @blocksuite/react doc for React components and hooks support.

Building

See BUILDING.md for instructions on how to build BlockSuite from source code.

License

MPL 2.0