1. Project Overview & Quickstart (toeverything/blocksuite)
File: README.md
BlockSuite
Overview
People who are really serious about editor should make their own framework.
BlockSuite is a toolkit for building editors and collaborative applications. It implements a series of content editing infrastructures, UI components and editors independently.
You can consider BlockSuite as a UI component library for building various editors, based on a minimized vanilla framework as their runtime. With BlockSuite, you can:
- Reuse multiple first-party BlockSuite editors:
PageEditor: A comprehensive block-based document editor, offering extensive customization and flexibility.EdgelessEditor: A graphics editor with opt-in canvas rendering support, but also shares the same rich-text capabilities with thePageEditor.
- Customize, extend and enhance these editors with a rich set of BlockSuite components and examples. All BlockSuite components (including editors) are native web components, making them framework-agnostic and easy to interop with popular frameworks.
- Or, build new editors from scratch based on the underlying vanilla framework.
π§ BlockSuite is currently in its early stage, with components and extension capabilities still under refinement. Hope you can stay tuned, try it out, or share your feedback!
Motivation
BlockSuite originated from the AFFiNE knowledge base, with design goals including:
- Support for Multimodal Editable Content: When considering knowledge as a single source of truth, building its various view modes (e.g., text, slides, mind maps, tables) still requires multiple incompatible frameworks. Ideally, no matter how the presentation of content changes, there should be a consistent framework that helps.
- Organizing and Visualizing Complex Knowledge: Existing editors generally focus on editing single documents, but often fall short in dealing with complex structures involving intertwined references. This requires the framework to natively manage state across multiple documents.
- Collaboration-Ready: Real-time collaboration is often seen as an optional plugin, but in reality, we could natively use the underlying CRDT technology for editor state management, which helps to build a clearer and more reliable data flow.
During the development of AFFiNE, it became clear that BlockSuite was advancing beyond merely being an in-house editor and evolving into a versatile framework. That's why we chose to open source and maintain BlockSuite independently.
Features
With BlockSuite editors, you can selectively reuse all the editing features in AFFiNE:
And under the hood, the vanilla BlockSuite framework supports:
- Defining custom blocks and inline embeds.
- Incremental updates, real-time collaboration, and even decentralized data synchronization based on the document streaming mechanism of the document.
- Writing type-safe complex editing logic based on the command mechanism, similar to react hooks designed for document editing.
- Persistence of documents and compatibility with various third-party formats (such as markdown and HTML) based on block snapshot and transformer.
- State scheduling across multiple documents and reusing one document in multiple editors.
To try out BlockSuite, refer to the quick start example and start with the preset editors in @blocksuite/presets.
Architecture
The relationship between BlockSuite and AFFiNE is similar to that between the Monaco Editor and VSCode, but with one major difference: BlockSuite is not automatically generated based on the AFFiNE codebase, but is maintained independently with a different tech stack β AFFiNE uses React while BlockSuite uses web components.
This difference has led BlockSuite to set clear boundaries based on a component-centric philosophy, ensuring:
- Both AFFiNE and other projects should equally reuse and extend BlockSuite through components, without any privileges.
- BlockSuite components can be easily reused regardless of whether you are using React or other frameworks.
To that end, the BlockSuite project is structured around key packages that are categorized into two groups: a headless framework and prebuilt editing components.
| Framework | |
|---|---|
@blocksuite/store |
Data layer for modeling collaborative document states. It is natively built on the CRDT library Yjs, powering all BlockSuite documents with built-in real-time collaboration and time-travel capabilities. |
@blocksuite/inline |
Minimal rich text components for inline editing. BlockSuite allows spliting rich text content in different block nodes into different inline editors, making complex content conveniently composable. This significantly reduces the complexity required to implement traditional rich text editing features. |
@blocksuite/block-std |
Framework-agnostic library for modeling editable blocks. Its capabilities cover the structure of block fields, events, selection, clipboard support, etc. |
| Components | |
|---|---|
@blocksuite/blocks |
Default block implementations for composing preset editors, including widgets belonging to each block. |
@blocksuite/presets |
Plug-and-play editable components including editors (PageEditor / EdgelessEditor) and auxiliary UI components named fragments (CopilotPanel, DocTitle...). |
Resources
- π Resources
- π Documentation
- π Good First Issues
- ποΈ GitHub Discussions
- π¬ Discord Channel
- π Releases
Building
See BUILDING.md for instructions on how to build and test BlockSuite from source.
Contributing
BlockSuite accepts pull requests on GitHub. Before you start contributing, please make sure you have read and accepted our Contributor License Agreement. To indicate your agreement, simply edit this file and submit a pull request.
License
File: docs/blog/crdt-native-data-flow.md
title: CRDT-Native Data Flow in BlockSuite
date: 2023-04-15
authors:
- name: Yifeng Wang
link: 'https://twitter.com/ewind1994' - name: Saul-Mirone
link: 'https://github.com/Saul-Mirone'
excerpt: To make editors intuitive and collaboration-ready, BlockSuite ensure that regardless of whether you are collaborating with others or not, the application code should be unaware of it. This article introduce how this is designed.
CRDT-Native Data Flow in BlockSuite
To make editors intuitive and collaboration-ready, BlockSuite ensure that regardless of whether you are collaborating with others or not, the application code should be unaware of it. This article introduce how this is designed.
CRDT as Single Source of Truth
Traditionally, CRDTs have often been seen as a technology specialized in conflict resolution. Many editors initially designed to support single users have implemented support for real-time collaboration by integrating CRDT libraries. To this end, the data models in these editors will be synchronized to the CRDTs. This usually involves two opposite data flows:
- When the local model is updated, the state of the native model is synchronized to the CRDT model.
- When a remote peer is updated, the data resolved from the CRDT model is synchronized back to the native model.
Although this is an intuitive and common practice, it requires synchronization between two heterogeneous models, resulting in a bidirectional data flow. The main issues here are:
- This bidirectional binding is not that easy to implement reliably and requires non-trivial modifications.
- Application-layer code often needs to distinguish whether an update comes from a remote source, which increases complexity.
As an alternative, BlockSuite chooses to directly use the CRDT model as the single source of truth (since BlockSuite uses Yjs, we also call it YModel here). This means that regardless of whether the update comes from local or remote sources, the same process will be performed:
- Firstly modify YModel, triggering the corresponding
Y.Eventthat contains all incremental state changes in this update. - Update the model nodes in the block tree based on the
Y.Event. - Send corresponding slot events after updating the block model, so as to update UI components accordingly.
This design can be represented by the following diagram:
The advantage of this approach is that the application-layer code can completely ignore whether updates to the block model come from local editing, history stack, or collaboration with other users. Just subscribing to model update events is adequate.
Case Study
As an example, suppose the current block tree structure is as follows:
RootBlock
NoteBlock
ParagraphBlock 0
ParagraphBlock 1
ParagraphBlock 2Now user A selects ParagraphBlock 2 and presses the delete key to delete it. At this point, doc.deleteBlock should be called to delete this block model instance:
const blockModel = doc.root.children[0].children[2];
doc.deleteBlock(blockModel);At this point, BlockSuite does not directly modify the block tree under doc.root, but will instead firstly modify the underlying YBlock. After the CRDT state is changed, Yjs will generate the corresponding Y.Event data structure, which contains all the incremental state changes in this update (similar to incremental patches in git and virtual DOM). BlockSuite will always use this as the basis to synchronize the block models, then trigger the corresponding slot events for UI updates.
In this example, as the parent of ParagraphBlock 2, the model.childrenUpdated slot event of NoteBlock will be triggered. This will enable the corresponding component in the UI framework component tree to refresh itself. Since each child block has an ID, this is very conducive to combining the common list key optimizations in UI frameworks, achieving on-demand block component updates.
But the real power lies in the fact that if this block tree is being concurrently edited by multiple people, when user B performs a similar operation, the corresponding update will be encoded by Yjs and distributed by the provider. When User A receives and applies the update from User B, the same state update pipeline as local editing will be triggered. This makes it unnecessary for the application to make any additional modifications or adaptations for collaboration scenarios, inherently gaining real-time collaboration capabilities.
Unidirectional Update Flow
Besides the block tree that uses CRDT as its single source of truth, BlockSuite also manages shared states that do not require a history of changes, such as the awareness state of each user's cursor position. Additionally, some user metadata may not be shared among all users.
In BlockSuite, the management of these state types follows a consistent, unidirectional pattern, enabling an intuitive one-way update flow that efficiently translates state changes into visual updates.
The complete state update process in BlockSuite involves several distinct steps, particularly when handling editor-related UI interactions:
- UI Event Handling: View components generate UI events like clicks and drags, initiating corresponding callbacks. In BlockSuite, it is recommended to model and reuse these interactions using commands.
- State Manipulation via Commands: Commands can manipulate the editor state to accomplish UI updates.
- State-Driven View Updates: Upon state changes, slot events are used to notify and update view components accordingly.
This update mechanism is depicted in the diagram above. Concepts such as command, view and event are further elaborated in other documentation sections for detailed understanding.
Summary
In summary, by utilizing the CRDT model as the single source of truth, the application layer code can remain agnostic to whether updates originate from local or remote sources. This simplifies synchronization and reduces complexity. This approach enables applications to acquire real-time collaboration capabilities without necessitating intrusive modifications or adaptations, which is a key reason why the BlockSuite editor has been inherently collaborative from day one.
File: docs/blog/document-centric.md
title: Building Document-Centric, CRDT-Native Editors
date: 2024-01-10
authors:
- name: Yifeng Wang
link: 'https://twitter.com/ewind1994'
excerpt: 'This article presents the document-centric way for building editors, and why CRDT is required to make this happpen.'
Building Document-Centric, CRDT-Native Editors
Motivation
For years, web frameworks such as React and Vue have popularized the mental model of component based development. This approach allows us to break down complex front-end applications into components for better composition and maintenance.
Hence, when discussing front-end collaborative editing (or rich text editing), the first thought is often to define an <Editor/> component, then design the corresponding data flow and APIs around this editor. This method seems intuitive and has been adopted by many open-source editors in the front-end community. Everything sounds natural, but are there limitations or room for improvement?
In the past years, our team has been dedicated to building a notable open-source knowledge base product (26k stars on GitHub). To visualize and organize complex knowledge structures better, we wanted our the editor in our product to be powerful enough, so as to provide an immersive editing and collaboration experience - imagine nesting Google Docs or Notion in an infinite canvas like Figma, as shown below:
However, before finding the best practice, our journey in developing editors was full of challenges. At first glance, the front-end community offers many great rich text editors (like Slate, Tiptap, Lexical) and whiteboard editors (like tldraw), which usually allows the embedding of React components. Bundling various React-compatible editors together seemed convenient - but proved impractical. To some extent, this is like trying to cram several devices supporting the USB protocol into the same shell. Despite sharing the same interface, there's no guarantee the resulting product will work correctly.
The frustration encountered in directly integrating various open-source editors led us to question the current design philosophy of popular editing frameworks. As a result, we decided to rebuild all necessary infrastructure for our editors, based on recent breakthroughs in collaborative editing technology (specifically, CRDT). The outcome was a powerful design pattern that no longer revolves around the editor. We call this approach document-centric.
The Document-Centric Approach
We believe that the current mainstream editing frameworks design their data flow around the <Editor/> component, with each editor managing its internal state cohesively. While this is a good design, some issues are hard to resolve:
- The data loading methods and internal state management mechanisms in different editors are not universal, making cross-editor state sharing difficult, often requiring redundant deep copies.
- Different editor containers have distinct internal life cycles, complicating the establishment of a consistent component model.
- The strong binding between document data and editor instances makes sharing a single document across multiple editor instances difficult, or managing multiple documents within a single editor instance.
- Although editors generally support embedding external components, nesting editors can easily lead to conflicts in focus, selection, shortcuts, etc.
Consider a simple example where a text editor A and an image editor B are used together:
For a simple user operation sequence:
- Perform several image editing operations.
- Delete the image, which will usually dispose its UI component.
- Continue editing the text.
If A and B are independently implemented editors, how should the user operation history be managed? Allowing A and B to maintain their history states seems easier, but neither can hold a complete user operation history. When an editor instance is destroyed, the history stack recording user operations generally disappears. Therefore, this often requires bookkeeping outside these editor instances, which is only the beginning of a series of complexities.
Alternatively, in the document-centric model, we believe that the document - the data layer of the editor, should be maintained completely independent of the editor, allowing the document to persist throughout the application lifecycle. Thus, no matter if a UI component is part of an editor or not, it should work by simply attaching to this document, like this:
Once the document is separated from the editor, it becomes easy to overcome many difficulties under the editor-centric approach:
- The above example is no longer a problem. Since the history record is stored in this persistently existing document, there's no need for bookkeeping between editor instances.
- Cross-editor state sharing can become zero-cost. Because the document (here we are referring to the editable content, not the global DOM variable) is also just a plain JavaScript object, which could be easily shared between different editor instances.
- Since editor instances are no longer strictly bound to document instances, rendering multiple documents in a single editor or displaying a single document across multiple editors becomes intuitively feasible.
In other words, the document-centric approach aims to establish a data layer that transcends editor boundaries, requiring various editors to drive their updates based on the (whole or partial) state of the external document, thus building a more flexible and diverse experience in a scalable way.
But given the complexity of collaborative document editing, is such architecture technically feasible?
Document-Centric and CRDT
Collaborative document editing is known for its complexity. Beyond handling user undo/redo history, traditional real-time collaboration requires complex algorithms like Operational Transformation to model editing actions into several restricted operations. Fortunately, CRDTs, which have made breakthrough progress in recent years, can encapsulate this complexity, making the document-centric model possible. In other words, we believe document-centric needs to be built on the foundation of CRDT.
Delving into the workings of CRDT is beyond the scope of this article. If you're unfamiliar with CRDT, all you need to know is that when used as a foundational library, CRDTs offer an experience and optimizations akin to standard JavaScript data types, much like the ImmutableJS.
Here's an example using ImmutableJS:
import Immutable from 'immutable';
let immutableMap = Immutable.Map({ key1: 'value1' });
immutableMap = immutableMap.set('key2', 'value2');
// { key1: 'value1', key2: 'value2' }
console.log(immutableMap.toJSON());And here's an (intuitively symmetrical) example using Yjs, a popular CRDT library:
import * as Y from 'yjs';
const yMap = new Y.Map();
yMap.set('key1', 'value1');
yMap.set('key2', 'value2');
// Supposed to be { key1: 'value1', key2: 'value2' }
console.log(yMap.toJSON());But be aware, this example won't work as expected! Here yMap.toJSON() will return an empty object. Because in Yjs, you actually need to create a Y.Doc first, then can you use CRDT data types like Y.Map / Y.Array / Y.Text:
import * as Y from 'yjs';
const yDoc = new Y.Doc();
// You need to `getMap` for top-level fields
const yMap = yDoc.getMap('hello');
yMap.set('key1', 'value1');
yMap.set('key2', 'value2');
// Only then can you attach nested data to doc nodes
yMap.set('key3', new Y.Map());
// { key1: 'value1', key2: 'value2', key3: {} }
console.log(yMap.toJSON());
// { hello: { key1: 'value1', key2: 'value2', key3: {} } }
console.log(yDoc.toJSON());To some extent, this API design is precisely a representation of the document-centric approach! Since all state changes are compulsively recorded on one persistently existing Y.Doc, it's highly apt for serving as the single source of truth for the state of UI components like editors. Documents based on Yjs have these capabilities:
- They can represent content structures equivalent to JSON, which includes maps, arrays, and various primitive data types in JavaScript.
- Rich text nodes (using
Y.Textinstead of juststring) can be optionally utilized within the document tree. - Highly granular event notifications are sent when document tree nodes are updated, potentially replacing the need for a virtual DOM!
- Documents can be serialized into a binary structure akin to protobuf or RSC payload (see y-protocols), and incremental encoding of partial updates to the document is also possible.
- In collaborative scenarios, these updates can be broadcast directly. Clients don't need to take care about the order of update application to achieve a consistent merged result (as guaranteed by the CRDT algorithm), enabling reliable real-time collaboration among multiple users.
As shown in the following diagram, the entire Y.Doc can be encoded into binary updates like the ones depicted, and all subsequent updates such as yMap.set() can also be incrementally encoded into the same binary patch:
This mechanism is similar to git. Each Y.Doc works like a git repository, and every operation on the CRDT document, such as yMap.set(), is akin to performing a git commit. This is because, like git, CRDT records all historical operations but without merge conflicts. Naturally, this also makes history management based on CRDT (akin to git revert) possible. These capabilities are sufficient for implementing a complete data layer based on CRDT.
Therefore, we chose to implement a common document data layer based on Yjs. This results in the following application data flow:
The blue part owns the full capability to drive UI in complex collaborative applications, including the management of rich text, history, conflict resolution, model update events, etc. This part has a well-defined isolation boundary from UI components and can be used independently of editors. We believe this is the data layer needed for being document-centric.
The BlockSuite Showcase
Embracing the document-centric philosophy, we created the BlockSuite project.
In BlockSuite, documents are modeled as doc objects. Each doc holds a tree of blocks. Some editor presets can be used upon connecting to a doc as following:
import { createEmptyDoc, PageEditor } from '@blocksuite/presets';
// Initialize a `doc` document
const doc = createEmptyDoc().init();
// Create an editor, then attach it to the document
const editor = new PageEditor();
editor.doc = doc;
document.body.appendChild(editor);BlockSuite advocates for assembling the top-level PageEditor component from smaller editable components, as all editable components can connect to different nodes in the block tree document. For example, instead of using existing complex rich text editors, BlockSuite implemented a @blocksuite/inline rich text component that only supports rendering linear text sequences. Complex rich text content can be assembled from atomic inline editor components, as illustrated:
In the diagram, each inline editor instance connects to a Y.Text node in the document tree. It models the data format of rich text as a linear sequence, with expressive power equivalent to the delta format. Thus, all rich text content in the document tree can be split into separate inline editors for rendering, eliminating the nesting between inline editors. This significantly lowers the cost of implementing rich text features, as depicted:
Since various editors can be loaded and unloaded independently of the document, this allows BlockSuite to support switching between different editors using the same block tree document. Thus, when switching content between document editors and whiteboard editors (which we call EdgelessEditor), all operation history recorded on the doc can be preserved, rather than reset:
Moreover, the separation of document and editor also allows docs to be used independently of editors. This is why BlockSuite not only provides various editor UI components but also many peripheral UI components that rely on doc state yet are not part of the editor. We refer to these components as fragments. The lifecycle of a fragment can be completely independent of the editor, and it can be implemented with a different technology stack than that used for the editor. For example, the right sidebar in the following diagram belongs to OutlineFragment, which facilitates panel arrangement by the application layer (rather than an all-in-one editor):
Furthermore, by supporting a document data layer independent of the editor, we are also able to split traditionally editor-embedded components into independent fragments, thus providing a more unopinionated and reusable PageEditor. Areas like the title and doc info panel, intuitively part of the editor's internals, can also become examples of fragments:
Additionally, the document-centric approach aids in better separation between the data layer and rendering layer, enabling developers to break free from the typically DOM-based editors, to implement better performance optimization strategies. For example, the BlockSuite document supports a surface block specially designed for rendering graphic content, which could take the advantage of the HTML5 <canvas>. BlockSuite allows these graphic contents to interleave with other block tree contents rendered to the DOM, automatically merging graphic elements into as few canvases as possible to enhance rendering performance:
In contrast, when there are 2000 canvas shapes in the document, tldraw, the DOM-based open-source whiteboard, would reaches its limit. At this point, it exhibits noticeable frame drops during viewport panning and zooming, degrading the content to placeholders with React suspense. However, the canvas renderer in BlockSuite could still maintain a frame rate of over 100fps at this time - and don't forget, you can still use the complete DOM-based rich text editing capability!
A year after creating BlockSuite, we have not only implemented a collaborative editing framework under the document-centric approach but also delivered an editor product with powerful document editing and canvas whiteboard editing capabilities. Considering the time traditionally required to implement complex rich text editors from scratch, we believe this is a highly efficient pattern. Of course, as a young open-source project, BlockSuite still has many areas for continuous improvement, and we hope you could stay tuned!
Summary
We explored the evolution of collaborative document editors, especially the transitioning from the traditional editor-centric approach to the document-centric approach. This transition implies several key points:
- Separation of Data and Editor: We emphasized the importance of separating the document data layer from the editor logic. Through this approach, document data becomes the core of the application, rather than being confined to a specific editor instance. This makes data sharing across editors and history management simple and efficient.
- Adoption of CRDT: Withe the help of CRDT, we demonstrated how to efficiently handle complex issues in collaborative editing, such as real-time synchronization and conflict resolution. CRDT provides a scalable way to build powerful multi-user editing experiences while maintaining eventual consistency.
- Flexible UI Construction: By separating the document data layer from the editor, we offered greater flexibility in building and optimizing user interfaces. Editors become pluggable components that can be flexibly assembled and configured according to specific application needs, creating richer and more dynamic user experiences.
We believe that the shift to document-centric not only solves some core issues faced by traditional editors but also opens up new possibilities for building future editing experiences. With this new design philosophy, developers can more flexibly build diverse collaborative tools while offering powerful, reliable, and seamless user experiences. As this pattern evolves, we look forward to seeing more innovative collaborative editing solutions emerge.
Support our project with a star π on GitHub: toeverything/blocksuite
File: docs/components/fragments/bi-directional-link-panel.md
Bi-Directional Link Panel
This component is an external panel for visualizing the bi-directional links related to the document.
::: info
π§ The comprehensive document of this component is still a work in progress.
:::
File: docs/components/fragments/copilot-panel.md
Copilot Panel
This component is an external panel for AI copilot.
::: info
π§ The comprehensive document of this component is still a work in progress.
:::
File: docs/components/fragments/doc-title.md
Doc Title
This component is used for displaying an editable doc title.
::: info
π§ The comprehensive document of this component is still a work in progress.
:::
File: docs/components/fragments/frame-panel.md
Frame Panel
This component is an external panel for visualizing the frames inside the surface block.
::: info
π§ The comprehensive document of this component is still a work in progress.
:::
File: docs/components/fragments/outline-panel.md
Outline Panel
This component is an external panel for visualizing the outline of the document.
::: info
π§ The comprehensive document of this component is still a work in progress.
:::
File: docs/components/editors/edgeless-data-structure.md
Edgeless Data Structure
Fundamentals
In BlockSuite, documents in edgeless mode are isomorphic to those in page mode. Edgeless documents are also composed of blocks, with no data conversion occurring during mode switching.
By default, the root block of a rich text document contains a single note block child, with a block tree structure like this:
Root Block
Note Block
Paragraph Block 1
Paragraph Block 2
Paragraph Block 3In the edgeless editor, you can easily split the document into multiple note cards, which can be positioned separately on the edgeless canvas, resulting in this block tree structure:
Root Block
Note Block
Paragraph Block 1
Note Block
Paragraph Block 2
Paragraph Block 3Surface Block and Surface Element
Edgeless mode introduces additional editable content such as brushes, connectors, and geometric shapes. To accommodate this, the edgeless editor implements a surface block as a container for whiteboard graphical content. Documents compatible with both edgeless and page modes have a default surface block as the first child of the root block:
Root Block
Surface Block
Note Block
Paragraph Block 1
Note Block
Paragraph Block 2
Paragraph Block 3The surface block can store two types of content:
- The
block.childrenfield can contain edgeless-specific card blocks, such as embed-style links to YouTube, Figma, or other BlockSuite documents. - Graphical content like brushstrokes and polygons are modeled as
SurfaceElements and stored in theblock.elementsfield. Common element types includeBrushElement,ShapeElement, andConnectorElement.
A typical edgeless document structure with a surface block might look like this:
Root Block
Surface Block
Embed Block
Shape Element
Brush Element
Note Block
Paragraph Block 1
Note Block
Paragraph Block 2
Paragraph Block 3Surface Block as Parent Block
With the introduction of the surface block, blocks can have two potential storage locations:
- Blocks used exclusively in the edgeless editor without nesting can be stored as direct children of the surface block.
- Other blocks are stored outside the surface block, as in rich text mode. These blocks can be reused between rich text and edgeless editor modes and allow complex nesting structures.
BlockSuite allows specific block types to appear in both locations. For example:
Root Block
Surface Block
Image Block 1
Note Block
Image Block 2
Image Block 3In the example above, the image blocks appear under both the note block and the surface block. The edgeless editor can display all three image blocks simultaneously. The key difference is that the image with the surface block as its parent can be placed anywhere on the whiteboard, while the images within the note block must be arranged from top to bottom according to the note's internal layout.
Block and Element Hierarchy
Although the block tree shows node adjacency, this relationship doesn't determine the content hierarchy in the whiteboard, which needs to render both blocks and surface elements.
Instead, BlockSuite allows specific block types to determine hierarchy order along with surface elements, including:
- Note blocks
- All children of the surface block
Blocks with adjustable hierarchy dynamically receive an index field. Since all surface elements also have this field, comparing the index values of these indexable blocks and elements uniquely determines the edgeless content hierarchy.
In this example:
Root Block
Surface Block
Brush Element
Image Block 1
Note Block
Paragraph Block 1
Paragraph Block 2
Image Block 2The note block and image block 1 have index fields and can adjust their hierarchy order along with the brush element. The paragraph blocks and image block 2 within the note block are rendered as children of the note block and don't intersect with surface elements hierarchically.
All indexable blocks and surface elements have an xywh field, determining their absolute position on the edgeless canvas.
This hierarchy determination technique is called fractional indexing, also used by Figma.
Frames and Groups
There are two ways to associate blocks and/or elements in the edgeless editor: frames and groups.
- Frame blocks are surface-only blocks with specific
xywhdimensions. Dragging a frame moves all blocks and elements within itsxywharea, establishing a dynamic association based on geometric region. Multiple frames can overlap positionally but cannot nest in the block tree. - Group elements are special surface elements without inherent dimensions, determined by their children. Groups store the IDs of all child nodes. All indexable blocks and elements can be group children, and groups can nest multiple levels.
Developers can extend these association mechanisms to create more dynamic nesting relationships, implementing element types like MindmapElement with more dynamic linking effects.
2. Official Technical Reference & Guides (toeverything/docs)
File: README.md
Welcome to the AFFiNE Docs!
β οΈ For reference only β οΈ
β οΈ This site and docs are not actively updated. β οΈ
\
β οΈ Please use our community site: β οΈ
π«± https://community.affine.pro π«²\
\
Welcome to the AFFiNE Docs!
β Getting Started General user information - including how to setup and get started using AFFiNE.
π» Developer Docs Looking to develop on/with AFFiNE? Make contributions on GitHub? Check out our dev docs.
π Internationalization Interested in our internationalization and helping translate AFFiNE to reach the world?
πΌ AFFiNE Ambassadors Want to help spread the word of AFFiNE and have access to talk with the team directly?
β’οΈ Branding Resources Some branding and style designs and resources for AFFiNE
Want to get in touch?
We are also active on various social and community platforms, head over to the communities page to find our official groups and pages:
{% content-ref url="community-links/official-communities.md" %}
official-communities.md
{% endcontent-ref %}
File: developer-docs/contributions/adding-ui-components/README.md
π Adding UI Components
AFFiNE defines a new component development specification in Figma, extends AFFiNE UI Components based on MUI BASE and MUI SYSTEM, and supplements as needed https://github.com/toeverything/AFFiNE/tree/master/libs/components/ui , e.g. src/libs/components/ui/src/button/base-button.ts.
import ButtonUnstyled, {
buttonUnstyledClasses,
} from '@mui/base/ButtonUnstyled';
import { styled } from '../styled';
/* eslint-disable @typescript-eslint/naming-convention */
const blue = {
500: '#007FFF',
600: '#0072E5',
700: '#0059B2',
};
/* eslint-enable @typescript-eslint/naming-convention */
export const BaseButton = styled(ButtonUnstyled)`
font-family: IBM Plex Sans, sans-serif;
font-weight: bold;
font-size: 0.875rem;
background-color: ${blue[500]};
border-radius: 8px;
padding: 4px 8px;
color: white;
transition: all 150ms ease;
cursor: pointer;
border: none;
&:hover {
background-color: ${blue[600]};
}
&.${buttonUnstyledClasses.active} {
background-color: ${blue[700]};
}
&.${buttonUnstyledClasses.focusVisible} {
box-shadow: 0 4px 20px 0 rgba(61, 71, 82, 0.1), 0 0 0 5px rgba(0, 127, 255, 0.5);
outline: none;
}
&.${buttonUnstyledClasses.disabled} {
opacity: 0.5;
cursor: not-allowed;
}
`;
export { BaseButton } from './base-button';File: developer-docs/contributions/making-code-contributions/README.md
Affine welcomes open source enthusiasts to collaborate and contribute interesting ideas and code.
β€ Making Code Contributions
What Does It Mean to Contribute [](#what-it-means-to-contribute)
If you are a new open source contributor, the process can be daunting - What if you don't know how to code? What if something goes wrong?
Don't worry! This article will provide detailed guidance for you.
Get Started
Create Issue
You should usually open an issue when you have trouble with:
- Presenting a problem you have that you cannot solve
- Presenting a valid idea, expectation, or conjecture about a topic
- Proposing a new feature
- Propose to improve or refactor existing code through code generation
Tips for communicating on issues:
- If you see an open issue that you want to tackle, comment on the issue. That way, people are less likely to duplicate your work.
- If an issue was opened a while ago, itβs possible that itβs being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work.
- If you opened an issue, but figured out the answer later on your own, comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project.
[**Fork Repository**](https://guides.github.com/activities/forking/)
You can fork the repository and clone the code repository to the local workspace.
Connect your local workspace to the original "upstream" repository by adding it as a remote.
Sync upstream branches from time to time keeps your code up to date. When you submit a pull request, the possibility of encountering conflict can be greatly reduced (see more detailed instructions here).
[**Create Branch**](https://guides.github.com/introduction/flow/) & Start Development
Create a local branch in your workspace and make the necessary changes.
Test Changes
Run your changes against any existing test and create new ones when needed. Whether tests exist or not, make sure your changes would not break the existing project.
It's important to maintain a consistent style with existing code (e.g. indentation, semicolons, the way comments are written), which makes it easier for maintainers to merge and for others to understand and maintain in the future.
[Open Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) [](#opening-a-pull-request)
Pull requests do not necessarily mean completed work. It is often better to open pull requests early on so that others can see your progress or give feedback. Simply mark it as "WIP" (Work in Progress) in the subject line. You can add more commits later.
- Reference any relevant issues or supporting documentation in your PR (for example, "Closes Issues#2345").
- If the view layer has changed, please include a comparison diagram of the difference before and after the change. If the performance metrics have changed, please detail the reasons for the change. So that the code review can be more focused.
{% hint style="info" %}
To ensure the security of our product, we kindly request that you share any scripts that may have been used to generate code changes. This allows us to audit the code and prevent any malicious code execution.
{% endhint %}
{% hint style="info" %}
Where appropriate, please consider submitting larger changes into smaller, separate pull requests.
{% endhint %}
Tips
If your pull request has not progressed for a long time (close or merge), please send the relevant information to [email protected].
Remember: you need to include the issue number in the content of your email.
File: developer-docs/contributions/README.md
π₯ Contributions
Welcome to the AFFiNE software development documentation.
Here you will find all the resources you need to start developing your AFFiNE project.
These pages will guide you through the stages of setting up your development environment, connecting to our code base, learning how to code for AFFiNE, and finally submitting code to the project!