feat(docs): migrate bs docs
@@ -52,6 +52,8 @@
|
|||||||
"packages/frontend/apps/ios/App/**",
|
"packages/frontend/apps/ios/App/**",
|
||||||
"tests/blocksuite/snapshots",
|
"tests/blocksuite/snapshots",
|
||||||
"blocksuite/docs/api/**",
|
"blocksuite/docs/api/**",
|
||||||
|
"blocksuite/docs-site/.vitepress/.temp/**",
|
||||||
|
"blocksuite/docs-site/api/**",
|
||||||
"packages/frontend/admin/src/config.json",
|
"packages/frontend/admin/src/config.json",
|
||||||
"**/test-docs.json",
|
"**/test-docs.json",
|
||||||
"**/test-blocks.json"
|
"**/test-blocks.json"
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ packages/frontend/apps/android/App/**
|
|||||||
packages/frontend/apps/ios/App/**
|
packages/frontend/apps/ios/App/**
|
||||||
tests/blocksuite/snapshots
|
tests/blocksuite/snapshots
|
||||||
blocksuite/docs/api/**
|
blocksuite/docs/api/**
|
||||||
|
blocksuite/docs-site/.vitepress/.temp/**
|
||||||
|
blocksuite/docs-site/api/**
|
||||||
packages/frontend/admin/src/config.json
|
packages/frontend/admin/src/config.json
|
||||||
**/test-docs.json
|
**/test-docs.json
|
||||||
**/test-blocks.json
|
**/test-blocks.json
|
||||||
|
|||||||
46
Cargo.lock
generated
@@ -48,7 +48,7 @@ dependencies = [
|
|||||||
"path-ext",
|
"path-ext",
|
||||||
"pdf-extract",
|
"pdf-extract",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rayon",
|
"rayon",
|
||||||
"readability",
|
"readability",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -94,7 +94,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"ogg",
|
"ogg",
|
||||||
"opus-codec",
|
"opus-codec",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rubato",
|
"rubato",
|
||||||
"screencapturekit",
|
"screencapturekit",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
@@ -204,7 +204,7 @@ dependencies = [
|
|||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3336,7 +3336,7 @@ version = "0.9.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064"
|
checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3431,7 +3431,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
"nom_locate",
|
"nom_locate",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rangemap",
|
"rangemap",
|
||||||
"sha2",
|
"sha2",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
@@ -3659,7 +3659,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3836,7 +3836,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -4236,7 +4236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.10.0",
|
"phf_shared 0.10.0",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4246,7 +4246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4572,7 +4572,7 @@ dependencies = [
|
|||||||
"bit-vec 0.8.0",
|
"bit-vec 0.8.0",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_xorshift",
|
"rand_xorshift",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
@@ -4687,9 +4687,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
@@ -4698,9 +4698,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
@@ -4751,7 +4751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5061,9 +5061,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.10"
|
version = "0.103.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -5557,7 +5557,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -5596,7 +5596,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -6062,9 +6062,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thin-vec"
|
name = "thin-vec"
|
||||||
version = "0.2.14"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
@@ -8146,7 +8146,7 @@ dependencies = [
|
|||||||
"path-ext",
|
"path-ext",
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -8168,7 +8168,7 @@ dependencies = [
|
|||||||
"phf 0.11.3",
|
"phf 0.11.3",
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"regex",
|
"regex",
|
||||||
"y-octo",
|
"y-octo",
|
||||||
|
|||||||
4
blocksuite/docs-site/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.vitepress/cache
|
||||||
|
.vitepress/dist
|
||||||
|
.vitepress/.temp
|
||||||
|
api/
|
||||||
134
blocksuite/docs-site/.vitepress/config.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import container from 'markdown-it-container';
|
||||||
|
import wasm from 'vite-plugin-wasm';
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
|
import { renderSandbox } from 'vitepress-plugin-sandpack';
|
||||||
|
|
||||||
|
import { components, guide, reference } from './sidebar';
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
// FIXME: remove dead links
|
||||||
|
ignoreDeadLinks: true,
|
||||||
|
|
||||||
|
title: 'BlockSuite',
|
||||||
|
description: 'Content Editing Tech Stack for the Web',
|
||||||
|
vite: {
|
||||||
|
build: {
|
||||||
|
target: 'ES2022',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
wasm(),
|
||||||
|
{
|
||||||
|
name: 'redirect-plugin',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
if (req.url === '/blocksuite-overview.html') {
|
||||||
|
res.writeHead(301, { Location: '/guide/overview.html' });
|
||||||
|
res.end();
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lang: 'en-US',
|
||||||
|
head: [
|
||||||
|
[
|
||||||
|
'link',
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '32x32',
|
||||||
|
href: 'https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['meta', { property: 'twitter:card', content: 'summary_large_image' }],
|
||||||
|
[
|
||||||
|
'meta',
|
||||||
|
{
|
||||||
|
property: 'twitter:image',
|
||||||
|
content:
|
||||||
|
'https://raw.githubusercontent.com/toeverything/blocksuite/master/packages/docs/images/blocksuite-cover.jpg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'meta',
|
||||||
|
{
|
||||||
|
property: 'og:image',
|
||||||
|
content:
|
||||||
|
'https://raw.githubusercontent.com/toeverything/blocksuite/master/packages/docs/images/blocksuite-cover.jpg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
themeConfig: {
|
||||||
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
|
outline: [2, 3],
|
||||||
|
|
||||||
|
nav: [
|
||||||
|
{
|
||||||
|
text: 'Components',
|
||||||
|
link: '/components/overview',
|
||||||
|
activeMatch: '/components/*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Framework',
|
||||||
|
link: '/guide/overview',
|
||||||
|
activeMatch: '/guide/*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Playground',
|
||||||
|
link: 'https://try-blocksuite.vercel.app/?init',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'More',
|
||||||
|
items: [
|
||||||
|
{ text: 'Blog', link: '/blog/', activeMatch: '/blog/*' },
|
||||||
|
{
|
||||||
|
text: 'API',
|
||||||
|
link: '/api/',
|
||||||
|
activeMatch: '/api/*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Releases',
|
||||||
|
link: 'https://github.com/toeverything/blocksuite/releases',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
sidebar: {
|
||||||
|
'/guide/': { base: '/', items: guide },
|
||||||
|
'/api/': { base: '/', items: reference },
|
||||||
|
'/components/': { base: '/', items: components },
|
||||||
|
},
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/toeverything/blocksuite' },
|
||||||
|
{
|
||||||
|
icon: {
|
||||||
|
svg: '<svg role="img" xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="#777777" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>',
|
||||||
|
},
|
||||||
|
link: 'https://twitter.com/AffineDev',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
copyright: 'Copyright © 2022-present Toeverything',
|
||||||
|
},
|
||||||
|
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
config(md) {
|
||||||
|
md.use(container, 'code-sandbox', {
|
||||||
|
render(tokens, idx) {
|
||||||
|
return renderSandbox(tokens, idx, 'code-sandbox');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
194
blocksuite/docs-site/.vitepress/sidebar.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import type { DefaultTheme } from 'vitepress';
|
||||||
|
|
||||||
|
export const guide: DefaultTheme.NavItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Introduction',
|
||||||
|
items: [
|
||||||
|
{ text: 'Overview', link: 'guide/overview' },
|
||||||
|
{ text: 'Quick Start', link: 'guide/quick-start' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Framework Guide',
|
||||||
|
items: [
|
||||||
|
{ text: 'Component Types', link: 'guide/component-types' },
|
||||||
|
{
|
||||||
|
text: 'Working with Block Tree',
|
||||||
|
// @ts-expect-error nested items are supported by VitePress at runtime
|
||||||
|
link: 'guide/working-with-block-tree',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Block Tree Basics',
|
||||||
|
link: 'guide/working-with-block-tree#block-tree-basics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Block Tree in Editor',
|
||||||
|
link: 'guide/working-with-block-tree#block-tree-in-editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Selecting Blocks',
|
||||||
|
link: 'guide/working-with-block-tree#selecting-blocks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Service and Commands',
|
||||||
|
link: 'guide/working-with-block-tree#service-and-commands',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Defining New Blocks',
|
||||||
|
link: 'guide/working-with-block-tree#defining-new-blocks',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ text: 'Data Synchronization', link: 'guide/data-synchronization' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Framework Handbook',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: '<code>block-std</code>',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Block Spec',
|
||||||
|
link: 'guide/block-spec',
|
||||||
|
// @ts-expect-error nested items are supported by VitePress at runtime
|
||||||
|
items: [
|
||||||
|
{ text: 'Block Schema', link: 'guide/block-schema' },
|
||||||
|
{ text: 'Block Service', link: 'guide/block-service' },
|
||||||
|
{ text: 'Block View', link: 'guide/block-view' },
|
||||||
|
{ text: 'Block Widgets', link: 'guide/block-widgets' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Selection',
|
||||||
|
link: 'guide/selection',
|
||||||
|
},
|
||||||
|
{ text: 'Event', link: 'guide/event' },
|
||||||
|
{ text: 'Command', link: 'guide/command' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '<code>store</code>',
|
||||||
|
items: [
|
||||||
|
{ text: 'Doc', link: 'guide/store#doc' },
|
||||||
|
{ text: 'DocCollection', link: 'guide/store#doccollection' },
|
||||||
|
{ text: 'Slot', link: 'guide/slot' },
|
||||||
|
{ text: 'Adapter', link: 'guide/adapter' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '<code>inline</code>',
|
||||||
|
link: 'guide/inline',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Developing BlockSuite',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Building Packages',
|
||||||
|
link: '//github.com/toeverything/blocksuite/blob/master/BUILDING.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Running Tests',
|
||||||
|
link: '//github.com/toeverything/blocksuite/blob/master/BUILDING.md#testing',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const reference: DefaultTheme.NavItem[] = [
|
||||||
|
{
|
||||||
|
text: 'API Reference',
|
||||||
|
items: [
|
||||||
|
{ text: '@blocksuite/store', link: 'api/@blocksuite/store/index' },
|
||||||
|
{
|
||||||
|
text: '@blocksuite/block-std',
|
||||||
|
link: 'api/@blocksuite/block-std/index',
|
||||||
|
},
|
||||||
|
{ text: '@blocksuite/inline', link: 'api/@blocksuite/inline/index' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const components: DefaultTheme.NavItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Introduction',
|
||||||
|
items: [{ text: 'Overview', link: 'components/overview' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Editors',
|
||||||
|
items: [
|
||||||
|
{ text: '📝 Page Editor', link: 'components/editors/page-editor' },
|
||||||
|
{
|
||||||
|
text: '🎨 Edgeless Editor',
|
||||||
|
// @ts-expect-error nested items are supported by VitePress at runtime
|
||||||
|
link: 'components/editors/edgeless-editor',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Data Structure',
|
||||||
|
link: 'components/editors/edgeless-data-structure',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Blocks',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Regular Blocks',
|
||||||
|
items: [
|
||||||
|
{ text: 'Root Block', link: 'components/blocks/root-block' },
|
||||||
|
{ text: 'Note Block', link: 'components/blocks/note-block' },
|
||||||
|
{
|
||||||
|
text: 'Paragraph Block',
|
||||||
|
link: 'components/blocks/paragraph-block',
|
||||||
|
},
|
||||||
|
{ text: 'List Block', link: 'components/blocks/list-block' },
|
||||||
|
{ text: 'Code Block', link: 'components/blocks/code-block' },
|
||||||
|
{ text: 'Image Block', link: 'components/blocks/image-block' },
|
||||||
|
{
|
||||||
|
text: 'Attachment Block',
|
||||||
|
link: 'components/blocks/attachment-block',
|
||||||
|
},
|
||||||
|
{ text: 'Divider Block', link: 'components/blocks/divider-block' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Advanced Blocks',
|
||||||
|
items: [
|
||||||
|
{ text: 'Surface Block', link: 'components/blocks/surface-block' },
|
||||||
|
{
|
||||||
|
text: 'Database Block',
|
||||||
|
link: 'components/blocks/database-block',
|
||||||
|
},
|
||||||
|
{ text: 'Frame Block', link: 'components/blocks/frame-block' },
|
||||||
|
{ text: 'Link Blocks', link: 'components/blocks/link-blocks' },
|
||||||
|
{ text: 'Embed Blocks', link: 'components/blocks/embed-blocks' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Widgets 🚧',
|
||||||
|
items: [
|
||||||
|
{ text: 'Slash Menu', link: 'components/widgets/slash-menu' },
|
||||||
|
{ text: 'Format Bar', link: 'components/widgets/format-bar' },
|
||||||
|
{ text: 'Drag Handle', link: 'components/widgets/drag-handle' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Fragments 🚧',
|
||||||
|
items: [
|
||||||
|
{ text: 'Doc Title', link: 'components/fragments/doc-title' },
|
||||||
|
{ text: 'Outline Panel', link: 'components/fragments/outline-panel' },
|
||||||
|
{ text: 'Frame Panel', link: 'components/fragments/frame-panel' },
|
||||||
|
{ text: 'Copilot Panel', link: 'components/fragments/copilot-panel' },
|
||||||
|
{
|
||||||
|
text: 'Bi-Directional Link Panel',
|
||||||
|
link: 'components/fragments/bi-directional-link-panel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
26
blocksuite/docs-site/.vitepress/theme/components/Icon.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const src = computed(() => {
|
||||||
|
if (props.icon) return props.icon;
|
||||||
|
return `https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${props.name.toLowerCase()}.svg`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img :src="src" :alt="`${name} Logo`" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
display: inline;
|
||||||
|
transform: translateY(5px);
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<h1>BlockSuite Blog</h1>
|
||||||
|
<div class="blog-posts-container">
|
||||||
|
<div class="blog-post" v-for="post in posts">
|
||||||
|
<a :href="post.url">
|
||||||
|
<h2 class="blog-post-title">{{ post.title }}</h2>
|
||||||
|
</a>
|
||||||
|
<div class="blog-post-excerpt">
|
||||||
|
{{ post.excerpt }}
|
||||||
|
<a class="blog-post-read-more" :href="post.url">Read more →</a>
|
||||||
|
</div>
|
||||||
|
<div class="blog-post-date">{{ post.date.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { usePosts } from '../composables/use-posts';
|
||||||
|
|
||||||
|
const { posts } = usePosts();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
font-size: 50px;
|
||||||
|
font-weight: bolder;
|
||||||
|
line-height: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-posts-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-date {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-excerpt {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-post {
|
||||||
|
border-bottom: 1px solid #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-read-more:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div class="blog-post-meta">
|
||||||
|
<span class="post-date">{{ post.date.formatted }}</span> by
|
||||||
|
<span v-html="formattedAuthors"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { usePosts } from '../composables/use-posts';
|
||||||
|
|
||||||
|
const { post } = usePosts();
|
||||||
|
|
||||||
|
const formattedAuthors = computed(() => {
|
||||||
|
return post.value.authors
|
||||||
|
.map(
|
||||||
|
author =>
|
||||||
|
`<a class="author" href="${author.link}" target="_blank">${author.name}</a>`
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.blog-post-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.author {
|
||||||
|
color: var(--vp-c-text-1) !important;
|
||||||
|
}
|
||||||
|
.post-date {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 'code-options' is a build-in prop, do not edit it -->
|
||||||
|
<Sandbox
|
||||||
|
:rtl="rtl"
|
||||||
|
:template="'vanilla-ts'"
|
||||||
|
:light-theme="lightTheme"
|
||||||
|
:dark-theme="darkTheme"
|
||||||
|
:options="{
|
||||||
|
...props, // do not forget it
|
||||||
|
coderHeight: Number(props.coderHeight),
|
||||||
|
previewHeight: Number(props.previewHeight),
|
||||||
|
showLineNumbers: true,
|
||||||
|
}"
|
||||||
|
:custom-setup="{
|
||||||
|
...props, // do not forget it
|
||||||
|
deps: {
|
||||||
|
yjs: 'latest',
|
||||||
|
'@toeverything/theme': 'latest',
|
||||||
|
'@blocksuite/presets': 'canary',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
:code-options="codeOptions"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Sandbox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Sandbox, sandboxProps } from 'vitepress-plugin-sandpack';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
...sandboxProps,
|
||||||
|
coderHeight: String,
|
||||||
|
previewHeight: String,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<img
|
||||||
|
style="width: 70%; height: 100%; margin: auto; opacity: 0.8"
|
||||||
|
src="https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { format, formatDistance } from 'date-fns';
|
||||||
|
import { createContentLoader } from 'vitepress';
|
||||||
|
|
||||||
|
interface Post {
|
||||||
|
title: string;
|
||||||
|
authors: { name: string; link: string }[];
|
||||||
|
url: string;
|
||||||
|
date: {
|
||||||
|
raw: string;
|
||||||
|
time: number;
|
||||||
|
formatted: string;
|
||||||
|
since: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(raw: string) {
|
||||||
|
const date = new Date(raw);
|
||||||
|
date.setUTCHours(8);
|
||||||
|
return {
|
||||||
|
raw: date.toISOString().split('T')[0],
|
||||||
|
time: +date,
|
||||||
|
formatted: format(date, 'yyyy/MM/dd'),
|
||||||
|
since: formatDistance(date, new Date(), { addSuffix: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [] as Post[];
|
||||||
|
export { data };
|
||||||
|
|
||||||
|
export default createContentLoader('blog/*.md', {
|
||||||
|
includeSrc: true,
|
||||||
|
transform(raw) {
|
||||||
|
return raw
|
||||||
|
.filter(item => item.url !== '/blog/')
|
||||||
|
.map(({ url, frontmatter }) => ({
|
||||||
|
title: frontmatter.title,
|
||||||
|
authors: frontmatter.authors ?? [],
|
||||||
|
excerpt: frontmatter.excerpt ?? '',
|
||||||
|
url,
|
||||||
|
date: formatDate(frontmatter.date),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.date.time - a.date.time);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useRoute } from 'vitepress';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { data as posts } from './posts.data';
|
||||||
|
|
||||||
|
export function usePosts() {
|
||||||
|
const route = useRoute();
|
||||||
|
const path = route.path;
|
||||||
|
|
||||||
|
function findCurrentIndex() {
|
||||||
|
const result = posts.findIndex(p => p.url === route.path);
|
||||||
|
if (result === -1) console.error(`blog post missing: ${route.path}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = computed(() => posts[findCurrentIndex()]);
|
||||||
|
|
||||||
|
return { posts, post, path };
|
||||||
|
}
|
||||||
29
blocksuite/docs-site/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import 'vitepress-plugin-sandpack/dist/style.css';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
import Theme from 'vitepress/theme';
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import BlogListLayout from './components/blog-list-layout.vue';
|
||||||
|
import BlogPostMeta from './components/blog-post-meta.vue';
|
||||||
|
import CodeSandbox from './components/code-sandbox.vue';
|
||||||
|
import HeroLogo from './components/hero-logo.vue';
|
||||||
|
import Icon from './components/icon.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Theme,
|
||||||
|
Layout: () => {
|
||||||
|
return h(Theme.Layout, null, {
|
||||||
|
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||||
|
'home-hero-image': () => h(HeroLogo),
|
||||||
|
// 'home-features-after': () => h(Playground),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enhanceApp({ app }) {
|
||||||
|
app.component('Icon', Icon);
|
||||||
|
app.component('BlogListLayout', BlogListLayout);
|
||||||
|
app.component('BlogPostMeta', BlogPostMeta);
|
||||||
|
app.component('CodeSandbox', CodeSandbox);
|
||||||
|
},
|
||||||
|
};
|
||||||
226
blocksuite/docs-site/.vitepress/theme/style.css
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-c-brand: #646cff;
|
||||||
|
--vp-c-brand-light: #747bff;
|
||||||
|
--vp-c-brand-lighter: #9499ff;
|
||||||
|
--vp-c-brand-lightest: #bcc0ff;
|
||||||
|
--vp-c-brand-dark: #535bf2;
|
||||||
|
--vp-c-brand-darker: #454ce1;
|
||||||
|
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-button-brand-border: var(--vp-c-brand-light);
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand);
|
||||||
|
--vp-button-brand-hover-border: var(--vp-c-brand-light);
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
|
||||||
|
--vp-button-brand-active-border: var(--vp-c-brand-light);
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||||
|
120deg,
|
||||||
|
#bd34fe 30%,
|
||||||
|
#41d1ff
|
||||||
|
);
|
||||||
|
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#bd34fe 50%,
|
||||||
|
#47caff 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(72px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VitePress: Custom fix
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use lighter colors for links in dark mode for a11y.
|
||||||
|
Also specify some classes twice to have higher specificity
|
||||||
|
over scoped class data attribute.
|
||||||
|
*/
|
||||||
|
.dark .vp-doc a,
|
||||||
|
.dark .vp-doc a > code,
|
||||||
|
.dark .VPNavBarMenuLink.VPNavBarMenuLink:hover,
|
||||||
|
.dark .VPNavBarMenuLink.VPNavBarMenuLink.active,
|
||||||
|
.dark .link.link:hover,
|
||||||
|
.dark .link.link.active,
|
||||||
|
.dark .edit-link-button.edit-link-button,
|
||||||
|
.dark .pager-link .title {
|
||||||
|
color: var(--vp-c-brand-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .vp-doc a:hover,
|
||||||
|
.dark .vp-doc a > code:hover {
|
||||||
|
color: var(--vp-c-brand-lightest);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition by color instead of opacity */
|
||||||
|
.dark .vp-doc .custom-block a {
|
||||||
|
transition: color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[class='dark'] {
|
||||||
|
--affine-theme-mode: dark;
|
||||||
|
|
||||||
|
--affine-popover-shadow:
|
||||||
|
0px 1px 10px -6px rgba(24, 39, 75, 0.08),
|
||||||
|
0px 3px 16px -6px rgba(24, 39, 75, 0.04);
|
||||||
|
--affine-font-h-1: 28px;
|
||||||
|
--affine-font-h-2: 26px;
|
||||||
|
--affine-font-h-3: 24px;
|
||||||
|
--affine-font-h-4: 22px;
|
||||||
|
--affine-font-h-5: 20px;
|
||||||
|
--affine-font-h-6: 18px;
|
||||||
|
--affine-font-base: 16px;
|
||||||
|
--affine-font-sm: 14px;
|
||||||
|
--affine-font-xs: 12px;
|
||||||
|
--affine-line-height: calc(1em + 8px);
|
||||||
|
--affine-z-index-modal: 1000;
|
||||||
|
--affine-z-index-popover: 1000;
|
||||||
|
--affine-font-family:
|
||||||
|
Avenir Next, Poppins, apple-system, BlinkMacSystemFont, Helvetica Neue,
|
||||||
|
Tahoma, PingFang SC, Microsoft Yahei, Arial, Hiragino Sans GB, sans-serif,
|
||||||
|
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
--affine-font-number-family:
|
||||||
|
Roboto Mono, apple-system, BlinkMacSystemFont, Helvetica Neue, Tahoma,
|
||||||
|
PingFang SC, Microsoft Yahei, Arial, Hiragino Sans GB, sans-serif,
|
||||||
|
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
--affine-font-code-family:
|
||||||
|
Space Mono, Consolas, Menlo, Monaco, Courier, monospace, apple-system,
|
||||||
|
BlinkMacSystemFont, Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei,
|
||||||
|
Arial, Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||||
|
Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
--affine-paragraph-space: 8px;
|
||||||
|
--affine-popover-radius: 10px;
|
||||||
|
--affine-zoom: 1;
|
||||||
|
--affine-scale: calc(1 / var(--affine-zoom));
|
||||||
|
|
||||||
|
--affine-brand-color: rgb(84, 56, 255);
|
||||||
|
--affine-primary-color: rgb(118, 95, 254);
|
||||||
|
--affine-secondary-color: rgb(144, 150, 245);
|
||||||
|
--affine-tertiary-color: rgb(30, 30, 30);
|
||||||
|
--affine-hover-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--affine-icon-color: rgb(168, 168, 160);
|
||||||
|
--affine-border-color: rgb(57, 57, 57);
|
||||||
|
--affine-divider-color: rgb(114, 114, 114);
|
||||||
|
--affine-placeholder-color: rgb(62, 62, 63);
|
||||||
|
--affine-quote-color: rgb(100, 95, 130);
|
||||||
|
--affine-link-color: rgb(185, 191, 227);
|
||||||
|
--affine-edgeless-grid-color: rgb(49, 49, 49);
|
||||||
|
--affine-success-color: rgb(77, 213, 181);
|
||||||
|
--affine-warning-color: rgb(255, 123, 77);
|
||||||
|
--affine-error-color: rgb(212, 140, 130);
|
||||||
|
--affine-processing-color: rgb(195, 215, 255);
|
||||||
|
--affine-text-emphasis-color: rgb(208, 205, 220);
|
||||||
|
--affine-text-primary-color: rgb(234, 234, 234);
|
||||||
|
--affine-text-secondary-color: rgb(156, 156, 160);
|
||||||
|
--affine-text-disable-color: rgb(119, 117, 125);
|
||||||
|
--affine-black-10: rgba(255, 255, 255, 0.1);
|
||||||
|
--affine-black-30: rgba(255, 255, 255, 0.3);
|
||||||
|
--affine-black-50: rgba(255, 255, 255, 0.5);
|
||||||
|
--affine-black-60: rgba(255, 255, 255, 0.6);
|
||||||
|
--affine-black-80: rgba(255, 255, 255, 0.8);
|
||||||
|
--affine-black-90: rgba(255, 255, 255, 0.9);
|
||||||
|
--affine-black: rgb(255, 255, 255);
|
||||||
|
--affine-white-10: rgba(0, 0, 0, 0.1);
|
||||||
|
--affine-white-30: rgba(0, 0, 0, 0.3);
|
||||||
|
--affine-white-50: rgba(0, 0, 0, 0.5);
|
||||||
|
--affine-white-60: rgba(0, 0, 0, 0.6);
|
||||||
|
--affine-white-80: rgba(0, 0, 0, 0.8);
|
||||||
|
--affine-white-90: rgba(0, 0, 0, 0.9);
|
||||||
|
--affine-white: rgb(0, 0, 0);
|
||||||
|
--affine-background-code-block: rgb(41, 44, 51);
|
||||||
|
--affine-background-tertiary-color: rgb(30, 30, 30);
|
||||||
|
--affine-background-processing-color: rgb(255, 255, 255);
|
||||||
|
--affine-background-error-color: rgb(255, 255, 255);
|
||||||
|
--affine-background-warning-color: rgb(255, 255, 255);
|
||||||
|
--affine-background-success-color: rgb(255, 255, 255);
|
||||||
|
--affine-background-primary-color: rgb(20, 20, 20);
|
||||||
|
--affine-background-hover-color: rgb(47, 47, 47);
|
||||||
|
--affine-background-secondary-color: rgb(32, 32, 32);
|
||||||
|
--affine-background-modal-color: rgba(0, 0, 0, 0.8);
|
||||||
|
--affine-background-overlay-panel-color: rgb(30, 30, 30);
|
||||||
|
--affine-tag-blue: rgb(10, 84, 170);
|
||||||
|
--affine-tag-green: rgb(55, 135, 79);
|
||||||
|
--affine-tag-teal: rgb(33, 145, 138);
|
||||||
|
--affine-tag-white: rgb(84, 84, 84);
|
||||||
|
--affine-tag-purple: rgb(59, 38, 141);
|
||||||
|
--affine-tag-red: rgb(139, 63, 63);
|
||||||
|
--affine-tag-pink: rgb(194, 132, 132);
|
||||||
|
--affine-tag-yellow: rgb(187, 165, 61);
|
||||||
|
--affine-tag-orange: rgb(231, 161, 58);
|
||||||
|
--affine-tag-gray: rgb(41, 41, 41);
|
||||||
|
--affine-palette-yellow: rgb(255, 232, 56);
|
||||||
|
--affine-palette-orange: rgb(255, 175, 56);
|
||||||
|
--affine-palette-tangerine: rgb(255, 99, 31);
|
||||||
|
--affine-palette-red: rgb(252, 63, 85);
|
||||||
|
--affine-palette-magenta: rgb(255, 56, 179);
|
||||||
|
--affine-palette-purple: rgb(182, 56, 255);
|
||||||
|
--affine-palette-navy: rgb(59, 37, 204);
|
||||||
|
--affine-palette-blue: rgb(79, 144, 255);
|
||||||
|
--affine-palette-green: rgb(16, 203, 134);
|
||||||
|
--affine-palette-grey: rgb(153, 153, 153);
|
||||||
|
--affine-palette-white: rgb(255, 255, 255);
|
||||||
|
--affine-palette-black: rgb(0, 0, 0);
|
||||||
|
}
|
||||||
87
blocksuite/docs-site/blog/crdt-native-data-flow.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
<BlogPostMeta />
|
||||||
|
|
||||||
|
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](https://github.com/yjs/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:
|
||||||
|
|
||||||
|
1. Firstly modify YModel, triggering the corresponding [`Y.Event`](https://docs.yjs.dev/api/y.event) that contains all incremental state changes in this update.
|
||||||
|
2. Update the model nodes in the block tree based on the `Y.Event`.
|
||||||
|
3. 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 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Now 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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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](https://docs.yjs.dev/api/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:
|
||||||
|
|
||||||
|
1. **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.
|
||||||
|
2. **State Manipulation via Commands**: Commands can manipulate the editor state to accomplish UI updates.
|
||||||
|
3. **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](../guide/command), [view](../guide/block-view) and [event](../guide/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.
|
||||||
196
blocksuite/docs-site/blog/document-centric.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
<BlogPostMeta />
|
||||||
|
|
||||||
|
## 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](https://github.com/toeverything/AFFiNE)). 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](https://github.com/ianstormtaylor/slate), [Tiptap](https://tiptap.dev/), [Lexical](https://lexical.dev/)) and whiteboard editors (like [tldraw](https://github.com/tldraw/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](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)). 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](https://en.wikipedia.org/wiki/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](https://immutable-js.com/).
|
||||||
|
|
||||||
|
Here's an example using ImmutableJS:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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](https://github.com/yjs/yjs), a popular CRDT library:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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.Text` instead of just `string`) 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](https://protobuf.dev/) or RSC payload (see [y-protocols](https://github.com/yjs/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](https://github.com/toeverything/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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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](https://quilljs.com/docs/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**](https://github.com/toeverything/blocksuite)
|
||||||
4
blocksuite/docs-site/blog/index.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
layout: BlogListLayout
|
||||||
|
title: Blog
|
||||||
|
---
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Attachment Block
|
||||||
|
|
||||||
|
This is a block used to place attachment content. It supports optional style configurations to display as cards of different styles.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`AttachmentBlockSchema`](/api/@blocksuite/blocks/variables/AttachmentBlockSchema.html)
|
||||||
|
- [`AttachmentBlockService`](/api/@blocksuite/blocks/classes/AttachmentBlockService.html)
|
||||||
7
blocksuite/docs-site/components/blocks/code-block.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Code Block
|
||||||
|
|
||||||
|
This is a block used to display code snippets. The code highlighting of different languages can be set through the `language` field.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`CodeBlockSchema`](/api/@blocksuite/blocks/variables/CodeBlockSchema.html)
|
||||||
10
blocksuite/docs-site/components/blocks/database-block.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Database Block
|
||||||
|
|
||||||
|
This is a block used to edit and display a data grid.
|
||||||
|
|
||||||
|
In a database block, each row is a standard paragraph block or list block. It supports adding different types of columns to rows and displaying a kanban view based on column information.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`DatabaseBlockSchema`](/api/@blocksuite/blocks/variables/DatabaseBlockSchema.html)
|
||||||
|
- [`DatabaseBlockService`](/api/@blocksuite/blocks/classes/DatabaseBlockService.html)
|
||||||
7
blocksuite/docs-site/components/blocks/divider-block.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Divider Block
|
||||||
|
|
||||||
|
This is a block used to display a divider line. It can be quickly obtained by typing `---` followed by pressing the space bar.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`DividerBlockSchema`](/api/@blocksuite/blocks/variables/DividerBlockSchema.html)
|
||||||
11
blocksuite/docs-site/components/blocks/embed-blocks.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Embed Blocks
|
||||||
|
|
||||||
|
These are blocks used to embed complex external content. The BlockSuite framework allows for the quick creation of embed blocks through [embed helper](../../guide/working-with-block-tree#defining-new-blocks).
|
||||||
|
|
||||||
|
Existing embed blocks that support BlockSuite document content include `EmbedLinkedDoc` and `EmbedSyncedDoc`, please refer to [link blocks](./link-blocks) for more details.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`EmbedYoutubeBlockProps`](/api/@blocksuite/blocks/type-aliases/EmbedYoutubeBlockProps.html)
|
||||||
|
- [`EmbedFigmaBlockProps`](/api/@blocksuite/blocks/type-aliases/EmbedFigmaBlockProps.html)
|
||||||
|
- [`EmbedGithubBlockProps`](/api/@blocksuite/blocks/type-aliases/EmbedGithubBlockProps.html)
|
||||||
11
blocksuite/docs-site/components/blocks/frame-block.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Frame Block
|
||||||
|
|
||||||
|
This is a block used to mark a canvas area in the edgeless editor.
|
||||||
|
|
||||||
|
When dragging the frame, the elements placed inside the frame move together with the frame. Note that there are no other blocks inside the frame. This association effect is based on the geometric area covered by the frame, not on a nested relationship at the model layer.
|
||||||
|
|
||||||
|
In presentation mode, the viewport will be moved to focus the frames one after another.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`FrameBlockSchema`](/api/@blocksuite/blocks/variables/FrameBlockSchema.html)
|
||||||
8
blocksuite/docs-site/components/blocks/image-block.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Image Block
|
||||||
|
|
||||||
|
This is a block used to display image content. It supports scaling in both the [page editor](../editors/page-editor) and the [edgeless editor](../editors/edgeless-editor), as well as an optional `caption`.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`ImageBlockSchema`](/api/@blocksuite/blocks/variables/ImageBlockSchema.html)
|
||||||
|
- [`ImageBlockService`](/api/@blocksuite/blocks/classes/ImageBlockService.html)
|
||||||
14
blocksuite/docs-site/components/blocks/link-blocks.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Link Blocks
|
||||||
|
|
||||||
|
These are blocks used to display link content in various ways.
|
||||||
|
|
||||||
|
- The `Bookmark` block supports displaying general web content with optional style configurations to display as cards of different styles.
|
||||||
|
- The `EmbedLinkedDoc` block supports embedding other BlockSuite documents as linked cards, also with various style configurations.
|
||||||
|
- The `EmbedSyncedDoc` block supports embedding other BlockSuite documents as editable sub-documents, based on the first-party transclusion support provided by BlockSuite.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`BookmarkBlockSchema`](/api/@blocksuite/blocks/variables/BookmarkBlockSchema.html)
|
||||||
|
- [`BookmarkBlockService`](/api/@blocksuite/blocks/classes/BookmarkBlockService.html)
|
||||||
|
- [`EmbedLinkedDocBlockProps`](/api/@blocksuite/blocks/type-aliases/EmbedLinkedDocBlockProps.html)
|
||||||
|
- [`EmbedSyncedDocBlockProps`](/api/@blocksuite/blocks/type-aliases/EmbedSyncedDocBlockProps.html)
|
||||||
12
blocksuite/docs-site/components/blocks/list-block.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# List Block
|
||||||
|
|
||||||
|
This is a block used to model list content with nesting support.
|
||||||
|
|
||||||
|
- When the `type` of this block is `bulleted`, it displays as bulleted list items.
|
||||||
|
- When the `type` of this block is `numbered`, it displays as numbered list items.
|
||||||
|
- When the `type` of this block is `todo`, it displays as todo list items.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`ListBlockSchema`](/api/@blocksuite/blocks/variables/ListBlockSchema.html)
|
||||||
|
- [`ListBlockService`](/api/@blocksuite/blocks/classes/ListBlockService.html)
|
||||||
14
blocksuite/docs-site/components/blocks/note-block.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Note Block
|
||||||
|
|
||||||
|
This is a container block used to place flowing document content.
|
||||||
|
|
||||||
|
If a document is entirely edited within the [page editor](../editors/page-editor), then all of its text content will be placed in a single note block. However, in the [edgeless editor](../editors/edgeless-editor), it allows for placing multiple notes on the canvas, and also for splitting the content of a single note block into multiple different notes.
|
||||||
|
|
||||||
|
In the page editor, the display order of notes is determined by the arrangement order of the note block in the root block `children`. But in the edgeless editor, the position of the note block is determined by the `xywh` field, and its layering with other graphical content is determined by the `index` field. This allows it to be positioned on the whiteboard along with other graphical content.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`NoteBlockSchema`](/api/@blocksuite/blocks/variables/NoteBlockSchema.html)
|
||||||
|
- [`NoteBlockService`](/api/@blocksuite/blocks/classes/NoteBlockService.html)
|
||||||
14
blocksuite/docs-site/components/blocks/paragraph-block.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Paragraph Block
|
||||||
|
|
||||||
|
This is a block used to place text content. It's generally placed inside a note block, but also supports nesting by pressing the tab key.
|
||||||
|
|
||||||
|
- When the `type` value of this block is `text`, it displays as a sequence of texts.
|
||||||
|
- When the `type` value of this block is `h1` / `h2` / `h3` / `h4` / `h5` / `h6`, it displays as titles of different levels.
|
||||||
|
- When the `type` value of this block is `quote`, it displays as a quote.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`ParagraphBlockSchema`](/api/@blocksuite/blocks/variables/ParagraphBlockSchema.html)
|
||||||
|
- [`ParagraphBlockService`](/api/@blocksuite/blocks/classes/ParagraphBlockService.html)
|
||||||
13
blocksuite/docs-site/components/blocks/root-block.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Root Block
|
||||||
|
|
||||||
|
This is the root node of the document tree, and its view implementation becomes the top-level UI of the editor.
|
||||||
|
|
||||||
|
For instance, the [page editor](../editors/page-editor) and the [edgeless editor](../editors/edgeless-editor) implement two different views for the root block. Generally, content (leaf nodes) is not placed directly in the root block. Its direct children are usually at least one [note block](./note-block) and one optional [surface block](./surface-block). Rich text content like paragraphs and lists are generally placed in the note blocks, while graphical content is placed in the surface block.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`RootBlockSchema`](/api/@blocksuite/blocks/variables/RootBlockSchema.html)
|
||||||
|
- [`PageRootService`](/api/@blocksuite/blocks/classes/PageRootService.html)
|
||||||
|
- [`EdgelessRootService`](/api/@blocksuite/blocks/classes/EdgelessRootService.html)
|
||||||
13
blocksuite/docs-site/components/blocks/surface-block.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Surface Block
|
||||||
|
|
||||||
|
This is a container used to render graphical content.
|
||||||
|
|
||||||
|
- In documents opened with the [edgeless editor](../editors/edgeless-editor), this block is required.
|
||||||
|
- Its `elements` field can contain a large number of `CanvasElement`s. These elements use HTML5 canvas for rendering and can be interleaved with note blocks, with automatically flattening to use the fewest canvas contexts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`SurfaceBlockSchema`](/api/@blocksuite/blocks/variables/SurfaceBlockSchema.html)
|
||||||
|
- [`SurfaceBlockService`](/api/@blocksuite/blocks/classes/SurfaceBlockService.html)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 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 3
|
||||||
|
```
|
||||||
|
|
||||||
|
In 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 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Surface 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 3
|
||||||
|
```
|
||||||
|
|
||||||
|
The surface block can store two types of content:
|
||||||
|
|
||||||
|
- The `block.children` field 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 `SurfaceElement`s and stored in the `block.elements` field. Common element types include `BrushElement`, `ShapeElement`, and `ConnectorElement`.
|
||||||
|
|
||||||
|
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 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Surface 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 3
|
||||||
|
```
|
||||||
|
|
||||||
|
In 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 2
|
||||||
|
```
|
||||||
|
|
||||||
|
The 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](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/).
|
||||||
|
|
||||||
|
## 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 `xywh` dimensions. Dragging a frame moves all blocks and elements within its `xywh` area, 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.
|
||||||
54
blocksuite/docs-site/components/editors/edgeless-editor.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Edgeless Editor
|
||||||
|
|
||||||
|
This editor component offers a canvas with infinite logical dimensions, suitable for whiteboard and graphic editing.
|
||||||
|
|
||||||
|
<iframe src="https://try-blocksuite.vercel.app/?init&mode=edgeless" frameborder="no" width="100%" height="500"></iframe>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- All the rich text editing capabilities in the [page editor](./page-editor).
|
||||||
|
- `CanvasElement` rendered to HTML5 canvas, including shapes, brushes, connectors, and text.
|
||||||
|
- Use of [frames](../blocks/frame-block) to denote canvas areas of any size.
|
||||||
|
- Presentation mode achieved by switching between multiple frames in sequence.
|
||||||
|
- Nestable group elements.
|
||||||
|
- Various [link cards](../blocks/link-blocks) that can be inserted on top of the canvas.
|
||||||
|
- Customizable toolbars and other widgets.
|
||||||
|
|
||||||
|
Moreover, this editor inherits capabilities built into the BlockSuite framework, including:
|
||||||
|
|
||||||
|
- Per-user undo/redo stack
|
||||||
|
- Real-time collaboration
|
||||||
|
- [Document streaming](../../guide/data-synchronization#document-streaming)
|
||||||
|
|
||||||
|
Notably, the BlockSuite framework allows runtime compatibility between the page editor and the edgeless editor, beyond mere static file format compatibility. This means you can dynamically attach the same doc object to different instances of the page editor and edgeless editor.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { EdgelessEditor } from '@blocksuite/presets';
|
||||||
|
|
||||||
|
const editor = new EdgelessEditor();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Like all BlockSuite editors, the editor UI is entirely composed of the combination of [block specs](../../guide/block-spec). A specialized [root block](../blocks/root-block) spec serves as the root node of the document and implements all top-level document UI, with main widgets also mounted on the root block. Accordingly, commonly used editing APIs are provided in the root service.
|
||||||
|
|
||||||
|
Specifically, the canvas element and some blocks that appear on the top layer of the canvas are located on the [surface block](../blocks/surface-block). Therefore, operating the edgeless editor also requires accessing the model and service mounted on this block.
|
||||||
|
|
||||||
|
To integrate and customize this editor, you can:
|
||||||
|
|
||||||
|
- [Customize new block specs](../../guide/working-with-block-tree#defining-new-blocks)
|
||||||
|
- 🚧 Configure widgets and customize new widgets
|
||||||
|
- 🚧 Use UI components from any framework
|
||||||
|
|
||||||
|
🚧 We are planning support for more frameworks.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`EdgelessEditor`](/api/@blocksuite/presets/classes/EdgelessEditor.html)
|
||||||
|
- [`EdgelessRootService`](/api/@blocksuite/blocks/classes/EdgelessRootService.html)
|
||||||
|
- [`SurfaceBlockModel`](/api/@blocksuite/blocks/classes/SurfaceBlockModel.html)
|
||||||
|
- [`SurfaceBlockService`](/api/@blocksuite/blocks/classes/SurfaceBlockService.html)
|
||||||
|
|
||||||
|
Since `EdgelessEditor` is a native web component, all DOM-related properties are inherited.
|
||||||
51
blocksuite/docs-site/components/editors/page-editor.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Doc Editor
|
||||||
|
|
||||||
|
This editor component is designed for conventional flow content editing, offering functionalities aligned with rich text editors based on the frameworks like ProseMirror or Slate.
|
||||||
|
|
||||||
|
<iframe src="https://try-blocksuite.vercel.app/?init" frameborder="no" width="100%" height="500"></iframe>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [Text](../blocks/paragraph-block), [lists](../blocks/list-block), and [code](../blocks/code-block) blocks, along with customizable inline elements.
|
||||||
|
- [Images](../blocks/image-block), [attachments](../blocks/attachment-block), and customizable [embed](../blocks/embed-blocks) blocks.
|
||||||
|
- [Database](../blocks/database-block) block that provides tables with kanban view support.
|
||||||
|
- Bidirectional [links](../blocks/link-blocks) between documents and transclusion similar to Notion synced blocks.
|
||||||
|
- Two types of selections, including native text selection and block-level selection.
|
||||||
|
- Cross-block dragging and multiple widget toolbars.
|
||||||
|
|
||||||
|
Moreover, this editor inherits capabilities built into the BlockSuite framework, including:
|
||||||
|
|
||||||
|
- Per-user undo/redo stack
|
||||||
|
- Real-time collaboration
|
||||||
|
- [Document streaming](../../guide/data-synchronization#document-streaming)
|
||||||
|
|
||||||
|
Notably, the BlockSuite framework allows runtime compatibility between the page editor and the edgeless editor, beyond mere static file format compatibility. This means you can dynamically attach the same doc object to different instances of the page editor and edgeless editor.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { PageEditor } from '@blocksuite/presets';
|
||||||
|
|
||||||
|
const editor = new PageEditor();
|
||||||
|
```
|
||||||
|
|
||||||
|
Assigning a [`doc`](../../guide/working-with-block-tree#block-tree-basics) object to `editor.doc` will attach a block tree to the editor, and [`editor.host`](../../guide/working-with-block-tree#block-tree-in-editor) contains the API surface for editing. The [quick start](../../guide/quick-start) guide also serves as an online playground.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Like all BlockSuite editors, the editor UI is entirely composed of the combination of [block specs](../../guide/block-spec). A specialized [root block](../blocks/root-block) spec serves as the root node of the document and implements all top-level document UI, with main widgets also mounted on the Accordingly, commonly used editing APIs are provided in the page service.
|
||||||
|
|
||||||
|
To integrate and customize this editor, you can:
|
||||||
|
|
||||||
|
- [Customize new block specs](../../guide/working-with-block-tree#defining-new-blocks)
|
||||||
|
- 🚧 Configure widgets and customize new widgets
|
||||||
|
- 🚧 Use UI components from any framework
|
||||||
|
|
||||||
|
🚧 We are planning support for more frameworks.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`PageEditor`](/api/@blocksuite/presets/classes/PageEditor.html)
|
||||||
|
- [`PageRootService`](/api/@blocksuite/blocks/classes/PageRootService.html)
|
||||||
|
|
||||||
|
Since `PageEditor` is a native web component, all DOM-related properties are inherited.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 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.
|
||||||
|
:::
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Copilot Panel
|
||||||
|
|
||||||
|
This component is an external panel for AI copilot.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🚧 The comprehensive document of this component is still a work in progress.
|
||||||
|
:::
|
||||||
9
blocksuite/docs-site/components/fragments/doc-title.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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.
|
||||||
|
:::
|
||||||
7
blocksuite/docs-site/components/fragments/frame-panel.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Frame Panel
|
||||||
|
|
||||||
|
This component is an external panel for visualizing the [frames](../blocks/frame-block) inside the [surface block](../blocks/surface-block).
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🚧 The comprehensive document of this component is still a work in progress.
|
||||||
|
:::
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 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.
|
||||||
|
:::
|
||||||
30
blocksuite/docs-site/components/overview.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# BlockSuite Components Overview
|
||||||
|
|
||||||
|
In a nutshell, BlockSuite categorizes components into the following types:
|
||||||
|
|
||||||
|
- **Editor** - A container used to present document content in various forms. Different editors are composed of different sets of [block specs](../guide/block-spec).
|
||||||
|
- **Block** - The atomic unit for constructing document within the editor. Once a [block spec](../guide/block-spec) is registered, multiple corresponding block instances can be rendered in the editor.
|
||||||
|
- **Widget** - Auxiliary components that contextually show up in the editor on demand, such as a search bar or color picker. Every block can define its own widgets.
|
||||||
|
- **Fragment** - External components outside the editor. They share the document with the editor but have their own lifecycles.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
These BlockSuite components are all implemented based on web components. For a more detailed description of the relationships between these components, please refer to the [component types](../guide/component-types) document.
|
||||||
|
|
||||||
|
## Extension and Customization
|
||||||
|
|
||||||
|
Based on the components API, BlockSuite allows:
|
||||||
|
|
||||||
|
- [Defining custom blocks](../guide/working-with-block-tree#defining-new-blocks) compatible with multiple editors.
|
||||||
|
- Configuring, extending, and replacing widgets within the editor, such as various toolbars, popups, and menus.
|
||||||
|
- Reusing components outside of the editor, such as panels for comments, outlines, or even AI copilots.
|
||||||
|
|
||||||
|
All BlockSuite components only need to be attached to the BlockSuite document model for use. For information on how to interact with this block tree, please refer to the [usage guide](../guide/working-with-block-tree) for the BlockSuite framework.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Regarding how BlockSuite components can be used in AFFiNE and other applications, here are some quick takeaways:
|
||||||
|
|
||||||
|
- The BlockSuite editor consists of various block specs, each of which can optionally include some widgets. Therefore, **when you are reusing an existing first-party BlockSuite editor, you are actually reusing a preset of blocks and widgets**. Default editors are fine-tuned presets for AFFiNE, but you are free to compose you own presets.
|
||||||
|
- Currently, all BlockSuite components are native web components, but we plan to provide official support for multiple frameworks.
|
||||||
|
- BlockSuite does not have special variants for AFFiNE, [we eat our own dogfood](https://gist.github.com/chitchcock/1281611).
|
||||||
7
blocksuite/docs-site/components/widgets/drag-handle.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Drag Handle
|
||||||
|
|
||||||
|
This widget is used for intuitive block-level dragging.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🚧 The comprehensive document of related components is still a work in progress.
|
||||||
|
:::
|
||||||
7
blocksuite/docs-site/components/widgets/format-bar.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Format Bar
|
||||||
|
|
||||||
|
This widget is used for inline text content formatting and quick actions.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🚧 The comprehensive document of related components is still a work in progress.
|
||||||
|
:::
|
||||||
17
blocksuite/docs-site/components/widgets/pie-menu.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Pie Menu
|
||||||
|
|
||||||
|
The Pie Menu widget offers users a visually intuitive and efficient way to access various functions through a single radial menu activated by a single key press. Inspired by Blender's interface, this widget offers enhanced functionality for streamlined workflow.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
- Trigger Key: Each menu is associated with a trigger key to open it. To activate a menu, press and hold the trigger key.
|
||||||
|
|
||||||
|
- Navigation: While holding the trigger key, move the cursor towards the desired action within the pie menu. Hovering over a segment of the pie menu highlights that action. Release the trigger key to select the highlighted action.
|
||||||
|
|
||||||
|
- Submenu: Submenus can be accessed by hovering over the submenu node (with a blue dot) within the pie menu. This allows for nested menu structures for organizing functions. To close a submenu just hover over the desired center node
|
||||||
|
|
||||||
|
- Keeping Menu Open: If the trigger key is released before the "SELECT_ON_RELEASE_TIMEOUT" threshold, the pie menu remains open, allowing for multiple selections without reopening the menu.
|
||||||
|
|
||||||
|
- Shortcut Selection: The numeric keys (0-9) correspond to actions within the pie menu. Pressing a numeric key selects the action associated with that number directly active (this does not work for color nodes).
|
||||||
|
|
||||||
|
Enhance your workflow by utilizing the Pie Menu widget for swift and intuitive access to various functions with minimal keystrokes.
|
||||||
7
blocksuite/docs-site/components/widgets/slash-menu.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Slash Menu
|
||||||
|
|
||||||
|
This widget is used for context menu triggered by slash (`/`) commands.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🚧 The comprehensive document of related components is still a work in progress.
|
||||||
|
:::
|
||||||
124
blocksuite/docs-site/guide/adapter.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Adapter
|
||||||
|
|
||||||
|
Adapter works as a bridge between different formats of data and the BlockSuite [`Snapshot`](./data-synchronization#snapshot-api) (i.e., the JSON-serialized block tree). It enables you to import and export data from and to BlockSuite documents.
|
||||||
|
|
||||||
|
## Base Adapter
|
||||||
|
|
||||||
|
[`BaseAdapter`](/api/@blocksuite/store/classes/BaseAdapter) provides you with a skeleton to build your own adapter. It is an abstract class that you can extend and implement the following methods:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export abstract class BaseAdapter<AdapterTarget = unknown> {
|
||||||
|
job: Job;
|
||||||
|
|
||||||
|
constructor(job: Job) {
|
||||||
|
this.job = job;
|
||||||
|
}
|
||||||
|
|
||||||
|
get configs() {
|
||||||
|
return this.job.adapterConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fromDocSnapshot(payload: FromDocSnapshotPayload): Promise<FromDocSnapshotResult<AdapterTarget>>;
|
||||||
|
abstract fromBlockSnapshot(payload: FromBlockSnapshotPayload): Promise<FromBlockSnapshotResult<AdapterTarget>>;
|
||||||
|
abstract fromSliceSnapshot(payload: FromSliceSnapshotPayload): Promise<FromSliceSnapshotResult<AdapterTarget>>;
|
||||||
|
abstract toDocSnapshot(payload: ToDocSnapshotPayload<AdapterTarget>): Promise<DocSnapshot>;
|
||||||
|
abstract toBlockSnapshot(payload: ToBlockSnapshotPayload<AdapterTarget>): Promise<BlockSnapshot>;
|
||||||
|
abstract toSliceSnapshot(payload: ToSliceSnapshotPayload<AdapterTarget>): Promise<SliceSnapshot | null>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Methods `fromDocSnapshot`, `fromBlockSnapshot`, `fromSliceSnapshot` are used to convert the data from the BlockSuite Snapshot to the target format. Methods `toDocSnapshot`, `toBlockSnapshot`, `toSliceSnapshot` are used to convert the data from the target format to the BlockSuite Snapshot.
|
||||||
|
|
||||||
|
Method `toSliceSnapshot` can return `null` if the target format cannot be converted to a slice using this adapter. It enables some components like clipboard to determine whether the adapter can handle the data. If not, it will try other adapters according to the priority.
|
||||||
|
|
||||||
|
These six core methods are expected to be purely functional. They should not have any side effects. If you need to change the behaviour of the adapter according to the job context, you can add it to `job.adapterConfigs` using job middlewares.
|
||||||
|
|
||||||
|
## Use Adapter
|
||||||
|
|
||||||
|
Sample usage:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const middleware: JobMiddleware = ({ adapterConfigs }) => {
|
||||||
|
// You can set the adapter configs here.
|
||||||
|
adapterConfigs.set('title:deadbeef', 'test');
|
||||||
|
};
|
||||||
|
|
||||||
|
const job = new Job({ collection: doc.collection, middlewares: [middleware] });
|
||||||
|
const snapshot = await job.docToSnapshot(doc);
|
||||||
|
|
||||||
|
const adapter = new MarkdownAdapter(job);
|
||||||
|
|
||||||
|
const markdownResult = await adapter.fromDocSnapshot({
|
||||||
|
snapshot,
|
||||||
|
assets: job.assetsManager,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## AST Walker
|
||||||
|
|
||||||
|
[ASTWalker](/api/@blocksuite/store/classes/ASTWalker) is a helper class that helps you to transform from and to different ASTs (Abstract Syntax Trees). For example, you can use it to transform from BlockSuite Snapshot (which can be treated as AST) to Markdown AST and then export to Markdown. Unlike other AST walkers, it does not only traverse the AST, but also gives you the ability to build a new AST with the data from the original AST.
|
||||||
|
|
||||||
|
It is recommended to use ASTWalker to build text-based adapters.
|
||||||
|
|
||||||
|
### Sample AST Walker
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ASTWalker } from '@blocksuite/store';
|
||||||
|
|
||||||
|
// ONode TNode
|
||||||
|
const walker = new ASTWalker<BlockSnapshot, MarkdownAST>();
|
||||||
|
|
||||||
|
// Make sure the leaves we are going to traverse are a type of BlockSnapshot.
|
||||||
|
// So it won't waste time on other properties.
|
||||||
|
walker.setONodeTypeGuard(
|
||||||
|
(node): node is BlockSnapshot =>
|
||||||
|
BlockSnapshotSchema.safeParse(node).success
|
||||||
|
);
|
||||||
|
|
||||||
|
walker.setEnter(async (o, context) => {
|
||||||
|
switch (o.node.flavour) {
|
||||||
|
case 'affine:list': {
|
||||||
|
context
|
||||||
|
.openNode(
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
value: convertToValue(o.node.props.text)
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
// Mount point for leaves
|
||||||
|
'children'
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
walker.setLeave(async (o, context) => {
|
||||||
|
switch (o.node.flavour) {
|
||||||
|
case 'affine:list': {
|
||||||
|
context.closeNode();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ast = await walker.walk(snapshot, markdown);
|
||||||
|
```
|
||||||
|
|
||||||
|
There are two handlers which will be called when the walker enters and leaves a node. Compared to a single handler, it gives you an elegant way to process nested nodes.
|
||||||
|
|
||||||
|
For example, consider a markdown document like this:
|
||||||
|
|
||||||
|
```md
|
||||||
|
- List 1 // context.openNode 1
|
||||||
|
- List 1.1 // context.openNode 2 && context.closeNode 2
|
||||||
|
- List 1.2 // context.openNode 3 && context.closeNode 3
|
||||||
|
// context.closeNode 1
|
||||||
|
- List 2 // context.openNode 4 && context.closeNode 4
|
||||||
|
```
|
||||||
|
|
||||||
|
The context works like a stack. In fact, it is a stack. When the walker enters a node, it will push the node to the stack. When the walker leaves a node, it will pop the node from the stack. Whenever the node pops from the stack, the walker will mount the node to its parent node.
|
||||||
|
|
||||||
|
In this case, the walker will push nodes when entering and pop nodes when leaving, producing a nested structure i.e. a tree.
|
||||||
|
|
||||||
|
In general, except for special cases, for the same `o.node.flavour`, `o.node.type` or something like this which can be used to identify a node's type, the number of `context.openNode` and `context.closeNode` should be the same. Otherwise, you likely have a bug in your code.
|
||||||
181
blocksuite/docs-site/guide/block-schema.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Block Schema
|
||||||
|
|
||||||
|
In BlockSuite, all blocks should have a schema. The schema of the block describes the data structure of the block.
|
||||||
|
|
||||||
|
You can use the `defineBlockSchema` function to define the schema of the block.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineBlockSchema } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
flavour: 'my-block',
|
||||||
|
props: internal => ({
|
||||||
|
text: internal.Text(),
|
||||||
|
level: 0,
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
version: 1,
|
||||||
|
role: 'content',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flavour and Props
|
||||||
|
|
||||||
|
Key takeaways for this part:
|
||||||
|
|
||||||
|
- The `flavour` of the block is a string that uniquely identifies the block. You can think of it as the name of the block.
|
||||||
|
- The `props` of the block are some attributes that the block has. They can be updated by some user actions. And they can be used to render the block. Some typical props are `text`, `level`, `url`, `src`, etc.
|
||||||
|
- You can use most of the primitive types in the props. But you should not use `undefined` or `null` in the props.
|
||||||
|
- We also support some special types in the props, called `internal` types. The internal types are used to describe some internal data structures of the block.
|
||||||
|
- `internal.Text` is a special type that represents the text of the block. It represents [Y.Text](https://docs.yjs.dev/api/shared-types/y.text) in the Yjs.
|
||||||
|
- You can also use arrays and objects in props.
|
||||||
|
|
||||||
|
## Schema Relations
|
||||||
|
|
||||||
|
You can also declare some relations between blocks in the schema.
|
||||||
|
|
||||||
|
### Role
|
||||||
|
|
||||||
|
You should declare a `role` for every block you create. The role of the block can be 3 values:
|
||||||
|
|
||||||
|
- `root`: The block is the root of the document. A document can only have one root block.
|
||||||
|
- `hub`: The block is a hub. A hub can have multiple children. The children of it can be either `hub` or `content`.
|
||||||
|
- `content`: The leaf block of the document. A content block can only have one parent. Also, it can only have `content` as its children.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
root
|
||||||
|
| hub1
|
||||||
|
| | content1
|
||||||
|
| | | content2
|
||||||
|
| hub2
|
||||||
|
| | hub3
|
||||||
|
| | | content3
|
||||||
|
| | content4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parent and Children
|
||||||
|
|
||||||
|
By default, a block will validate its children and parent by its `role`. You can also pass a `parent` or `children` option to the schema to override the default behaviour.
|
||||||
|
|
||||||
|
Some examples:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This means the block's children must match the flavour `my-leaf`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineBlockSchema } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
// ...
|
||||||
|
metadata: {
|
||||||
|
children: ['my-leaf'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
When passing `*`, it means all blocks that match the rule of `role` can be used.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
// ...
|
||||||
|
metadata: {
|
||||||
|
children: ['*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
You can also pass glob patterns:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
// ...
|
||||||
|
metadata: {
|
||||||
|
children: ['my-data-*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The glob match feature is powered by [minimatch](https://github.com/isaacs/minimatch).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This means the block won't accept any children.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
// ...
|
||||||
|
metadata: {
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema to Model
|
||||||
|
|
||||||
|
The schema of the block is used to generate the model of the block. By default, the model will holds the flavour, props and id of the block.
|
||||||
|
|
||||||
|
```
|
||||||
|
MyBlockSchema
|
||||||
|
-> MyBlockModel-1
|
||||||
|
-> MyBlockModel-2
|
||||||
|
-> MyBlockModel-3
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if we have a schema like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineBlockSchema, type Text } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export type MyBlockProps = {
|
||||||
|
text: Text;
|
||||||
|
level: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MyBlockSchema = defineBlockSchema({
|
||||||
|
flavour: 'my-block',
|
||||||
|
props: (internal): MyBlockProps => ({
|
||||||
|
text: internal.Text(),
|
||||||
|
level: 0,
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
version: 1,
|
||||||
|
role: 'content',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
And when the model is created, you can use it like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { type SchemaToModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
function doSomething(model: SchemaToModel<typeof MyBlockSchema>) {
|
||||||
|
const id = model.id;
|
||||||
|
const flavour = model.flavour;
|
||||||
|
const text = model.text;
|
||||||
|
const level = model.level;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also customize the model by extending the `BlockModel` to provide more methods:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export class MyBlockModel extends BlockModel<MyBlockProps> {
|
||||||
|
levelUp() {
|
||||||
|
this.level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSomething(model: MyBlockModel) {
|
||||||
|
model.levelUp();
|
||||||
|
const level = model.level;
|
||||||
|
}
|
||||||
|
```
|
||||||
67
blocksuite/docs-site/guide/block-service.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Block Service
|
||||||
|
|
||||||
|
Each kind of block can register its own service, so as to define block-specific methods to be called during the editor lifecycle. The service is a class that extends the `BlockService` class:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BlockService } from '@blocksuite/block-std';
|
||||||
|
import { defineBlockSchema, type SchemaToModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
const myBlockSchema = defineBlockSchema({
|
||||||
|
//...
|
||||||
|
});
|
||||||
|
|
||||||
|
type MyBlockModel = SchemaToModel<typeof myBlockSchema>;
|
||||||
|
|
||||||
|
class MyBlockService extends BlockService<MyBlockModel> {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For each block type, its service will only be instantiated once. And even though there is no block instance, the service will still be instantiated. So it's designed for defining editor-level methods for certain kind of block.
|
||||||
|
|
||||||
|
For example, if you want to bind certain hotkey for creating a new block, you can do it in the service:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyBlockService extends BlockService<MyBlockModel> {
|
||||||
|
override mounted() {
|
||||||
|
super.mounted();
|
||||||
|
this.bindHotkey(
|
||||||
|
{
|
||||||
|
'Alt-1': this._addMyBlock,
|
||||||
|
},
|
||||||
|
{ global: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addMyBlock = () => {
|
||||||
|
this.doc.addBlock('my-block', {});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Hooks
|
||||||
|
|
||||||
|
The `BlockService` class provides some lifecycle hooks for you to override.
|
||||||
|
|
||||||
|
- `mounted`: This hook will be called when the service is instantiated.
|
||||||
|
- `unmounted`: This hook will be called when the service is destroyed.
|
||||||
|
|
||||||
|
## Set Runtime Configs
|
||||||
|
|
||||||
|
Sometimes you may want to set some runtime configurations for some blocks to better fit your needs.
|
||||||
|
|
||||||
|
For example, you may want to set an image proxy middleware URL for the image block. By default the image block will use AFFiNE's image proxy to bypass CORS restrictions. In the self-hosted case, you may want to set your own image proxy middleware URL concerning that the default one will not be available:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ImageService } from '@blocksuite/blocks';
|
||||||
|
|
||||||
|
const editorRoot = document.querySelector('editor-host');
|
||||||
|
if (!editorRoot) return;
|
||||||
|
|
||||||
|
const imageService = editorRoot.spec.getService('affine:image') as ImageService;
|
||||||
|
|
||||||
|
// Call specific method to set runtime configurations
|
||||||
|
imageService.setImageProxyURL('https://example.com/image-proxy');
|
||||||
|
```
|
||||||
|
|
||||||
|
For different blocks, the method to set runtime configurations may be different. You can check the [block API document](/api/@blocksuite/blocks/index) to find out the methods you need.
|
||||||
38
blocksuite/docs-site/guide/block-spec.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Block Spec
|
||||||
|
|
||||||
|
In BlockSuite, a `BlockSpec` defines the structure and interactive elements for a specific block type within the editor. BlockSuite editors are typically composed entirely of block specs, with the top-level UI often implemented as a dedicated block, usually of the `affine:page` type.
|
||||||
|
|
||||||
|
A block spec contains the following properties:
|
||||||
|
|
||||||
|
- [`schema`](./block-schema): Defines the structure and data types for the block's content.
|
||||||
|
- [`service`](./block-service): Used for registering methods for specific actions and external invocations.
|
||||||
|
- [`view`](./block-view): Represents the visual representation and layout of the block.
|
||||||
|
- `component`: The primary user interface element of the block.
|
||||||
|
- `widgets`: Additional interactive elements enhancing the block's functionality.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Note that in block spec, the definition of `view` is related to UI frameworks. By default, we provide a `@blocksuite/lit` package to help build a lit block view. But it's still possible to use other UI frameworks. We'll introduce later about how to write custom block renderers.
|
||||||
|
|
||||||
|
Here is a example of a lit-based block spec:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { BlockSpec } from '@blocksuite/block-std';
|
||||||
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
const MyBlockSepc: BlockSpec = {
|
||||||
|
schema: MyBlockSchema,
|
||||||
|
service: MyBlockService,
|
||||||
|
view: {
|
||||||
|
component: literal`my-block-component`,
|
||||||
|
widgets: {
|
||||||
|
myBlockToolbar: literal`my-block-toolbar`,
|
||||||
|
myBlockMenu: literal`my-block-menu`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll introduce each part of the block spec in the following sections.
|
||||||
108
blocksuite/docs-site/guide/block-view.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Block View
|
||||||
|
|
||||||
|
In BlockSuite, blocks can be rendered by any UI framework. A block should be rendered to a DOM element, and we use `view` to represent the renderer.
|
||||||
|
|
||||||
|
By default, we provide a [lit](https://lit.dev/) renderer called `@blocksuite/lit`. But it's still possible to use other UI frameworks. We'll introduce later about how to write custom block renderers.
|
||||||
|
|
||||||
|
## Web Component Block View
|
||||||
|
|
||||||
|
We provide a `BlockComponent` class to help building a lit-based block view.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineBlockSchema, type SchemaToModel } from '@blocksuite/store';
|
||||||
|
import { BlockComponent } from '@blocksuite/lit';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
const myBlockSchema = defineBlockSchema({
|
||||||
|
//...
|
||||||
|
props: () => ({
|
||||||
|
count: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MyBlockModel = SchemaToModel<typeof myBlockSchema>;
|
||||||
|
|
||||||
|
@customElements('my-block')
|
||||||
|
class MyBlockView extends BlockComponent<MyBlockModel> {
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h3>My Block</h3>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Render Children
|
||||||
|
|
||||||
|
A block can have children, and we can render them by using `renderModelChildren`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@customElements('my-block')
|
||||||
|
class MyBlockView extends BlockComponent<MyBlockModel> {
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h3>My Block</h3>
|
||||||
|
${this.renderModelChildren(this.model)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get and Set Props
|
||||||
|
|
||||||
|
It's easy to get and set props in a block view.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@customElements('my-block')
|
||||||
|
class MyBlockView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _onClick = () => {
|
||||||
|
this.doc.updateBlock(this.model, {
|
||||||
|
count: this.model.count + 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h3>My Block</h3>
|
||||||
|
<p>Count: ${this.model.count}</p>
|
||||||
|
<button @click=${this._onClick}>Add</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It's also possible to watch prop changes to create something like `computed props`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@customElements('my-block')
|
||||||
|
class MyBlockView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _yen = '0¥';
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
this.model.propsUpdated.on(() => {
|
||||||
|
this._yen = `${this.model.count * 100}¥`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h3>My Block</h3>
|
||||||
|
<p>Price: ${this._yen}</p>
|
||||||
|
<button @click=${this._onClick}>Add</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get the `std` instance from `this.std` to use the full power of [`block-std`](/api/@blocksuite/block-std/).
|
||||||
59
blocksuite/docs-site/guide/block-widgets.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Block Widgets
|
||||||
|
|
||||||
|
In BlockSuite, widgets are components that can be used to display helper UI elements of a block. Sometimes, you want to display a menu to provide some extra information or actions for a block. As another example, it's a common practice to display a toolbar when you select a block.
|
||||||
|
|
||||||
|
The widget is designed to provide this kind of functionalities. Similar to blocks, widgets also depends on UI frameworks. By default, we provide a [lit](https://lit.dev/) renderer called `@blocksuite/lit` for building widgets as web components. But it's still possible to use other UI frameworks. We'll introduce later about implementing custom block renderers.
|
||||||
|
|
||||||
|
## Widget Component
|
||||||
|
|
||||||
|
The `WidgetComponent` class can be used for building a widget view based on web component:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { WidgetComponent } from '@blocksuite/lit';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElements('my-widget')
|
||||||
|
class MyWidgetView extends WidgetComponent<MyBlockView> {
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h3>My Widget</h3>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get Host Block
|
||||||
|
|
||||||
|
Widget is always related to a block called host block.
|
||||||
|
And we can get the host block by using `BlockComponent` property.
|
||||||
|
|
||||||
|
For example, if you have a `code block` for displaying code examples, and you want to display a `language picker` widget to let users change the language of the code block. The widget could be defined in this manner:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { WidgetComponent } from '@blocksuite/lit';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElements('my-widget')
|
||||||
|
class CodeLanguagePicker extends WidgetComponent<CodeBlockComponent> {
|
||||||
|
private _onChange = e => {
|
||||||
|
this.doc.updateBlock(this.blockComponent.model, {
|
||||||
|
language: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<select @change=${this._onChange}>
|
||||||
|
<option value="javascript">JavaScript</option>
|
||||||
|
<option value="python">Python</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get the `std` instance from `this.std` to use the full power of [`block-std`](/api/@blocksuite/block-std/).
|
||||||
187
blocksuite/docs-site/guide/command.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Command
|
||||||
|
|
||||||
|
Commands are the reusable actions for triggering state updates. Inside a command, you can query different states of the editor, or perform operations to update them. With the command API, you can define chainable commands and execute them.
|
||||||
|
|
||||||
|
## Command Chain
|
||||||
|
|
||||||
|
Commands are executed in a chain, and each command can decide whether to continue the chain or not.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
std.command.chain().command1().command2().command3().run();
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to call `chain()` to start a new chain. Then, you can call any command defined in the `Commands` interface. And finally, call `run()` to execute the chain.
|
||||||
|
|
||||||
|
### Try
|
||||||
|
|
||||||
|
If a command fails, the chain will be interrupted. However, you can use `try()` to call a list of commands until one of them succeeds.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
std.command
|
||||||
|
.chain()
|
||||||
|
.try(cmd => [cmd.command1(), cmd.command2()])
|
||||||
|
.command3()
|
||||||
|
.run();
|
||||||
|
```
|
||||||
|
|
||||||
|
In this chain, `command3` will be executed only if `command1` or `command2` succeeds. If `command1` succeeds, `command2` will not be executed.
|
||||||
|
|
||||||
|
### TryAll
|
||||||
|
|
||||||
|
`tryAll` is used to attempt to execute an array of commands within a chain. Unlike `try`, which stops executing the list of commands as soon as one of them succeeds, `tryAll` will execute every command in the array, regardless of the individual outcomes of each command.
|
||||||
|
|
||||||
|
This means that even if one of the commands succeeds, `tryAll` will still continue to execute the remaining commands in the array. The chain will only proceed to the next command after `tryAll` if at least one command in the array succeeds. If all commands fail, the chain will be interrupted.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
std.command
|
||||||
|
.chain()
|
||||||
|
.tryAll(cmd => [cmd.command1(), cmd.command2(), cmd.command3()])
|
||||||
|
.command4()
|
||||||
|
.run();
|
||||||
|
```
|
||||||
|
|
||||||
|
If `command1`, `command2`, or `command3` succeeds, `command4` will be executed. If all commands in `tryAll` fail, the chain will stop, and `command4` will not be executed.
|
||||||
|
|
||||||
|
Use `tryAll` when you want to ensure that multiple strategies or operations are attempted, even if the success of one is enough to allow the chain to continue. This approach is useful when each command in the array should be given a chance to execute, regardless of the success of the others.
|
||||||
|
|
||||||
|
## Writing Commands
|
||||||
|
|
||||||
|
Commands are defined as pure functions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Command } from '@blocksuite/block-std';
|
||||||
|
export const myCommand: Command = (ctx, next) => {
|
||||||
|
if (fail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace BlockSuite {
|
||||||
|
interface Commands {
|
||||||
|
my: typeof myCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the command to the std command list
|
||||||
|
std.command.add('my', myCommand);
|
||||||
|
|
||||||
|
// You can call it with
|
||||||
|
std.command.chain().my().run();
|
||||||
|
```
|
||||||
|
|
||||||
|
Only when the command calls `next()`, the next command in the chain will be executed.
|
||||||
|
|
||||||
|
## Command Context
|
||||||
|
|
||||||
|
When a list of commands are executed, they share a context object.
|
||||||
|
This object is standalone for each command execution, and you can use it to store temporary data.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Command } from '@blocksuite/block-std';
|
||||||
|
export const myCommand: Command<never, 'myCommandData'> = (ctx, next) => {
|
||||||
|
if (fail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({ myCommandData: 'hello' });
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace BlockSuite {
|
||||||
|
interface CommandContext {
|
||||||
|
myCommandData: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands {
|
||||||
|
myCommand: typeof myCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, commands executed after `myCommand` can access the data:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const myCommand: Command<'myCommandData'> = (ctx, next) => {
|
||||||
|
const data = ctx.myCommandData;
|
||||||
|
console.log(data);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Options
|
||||||
|
|
||||||
|
You can pass options to a command when calling it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Command } from '@blocksuite/block-std';
|
||||||
|
|
||||||
|
type MyCommandOptions = {
|
||||||
|
configA: number;
|
||||||
|
configB: string;
|
||||||
|
};
|
||||||
|
export const myCommand: Command<never, never, MyCommandOptions> = (ctx, next) => {
|
||||||
|
const { configA, configB } = ctx;
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// You can call it with
|
||||||
|
std.command.chain().my({ configA: 0, configB: 'hello' }).run();
|
||||||
|
```
|
||||||
|
|
||||||
|
Please notice that commands take only one argument,
|
||||||
|
so you need to wrap the options in an object if you want to pass multiple options.
|
||||||
|
|
||||||
|
## Inline Command
|
||||||
|
|
||||||
|
You can also use inline command for some temporary commands.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
std.command
|
||||||
|
.chain()
|
||||||
|
.inline((ctx, next) => {
|
||||||
|
// ...
|
||||||
|
return next();
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Returns
|
||||||
|
|
||||||
|
After `.run`, the command chain will return two values: `success` and `ctx`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [success, ctx] = std.command.chain().commandA().commandB().run();
|
||||||
|
```
|
||||||
|
|
||||||
|
If all commands passed, the `success` will be `true`, otherwise it will be `false`.
|
||||||
|
|
||||||
|
The `ctx` will be the final `context` updated by `.next` in a command chain.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const command1 = (ctx, next) => {
|
||||||
|
return next({ data: 0, str: 'hello' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const command2 = (ctx, next) => {
|
||||||
|
return next({ data: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const [success, ctx] = std.command.chain().command1().command2().run();
|
||||||
|
|
||||||
|
// This will pass
|
||||||
|
expect(ctx.data).toBe(1);
|
||||||
|
|
||||||
|
// This will pass too
|
||||||
|
expect(ctx.str).toBe('hello');
|
||||||
|
```
|
||||||
78
blocksuite/docs-site/guide/component-types.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# BlockSuite Component Types
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🌐 This documentation has a [Chinese translation](https://insider.affine.pro/share/af3478a2-9c9c-4d16-864d-bffa1eb10eb6/94-Y53OqW0NFm6l-wqDz6).
|
||||||
|
:::
|
||||||
|
|
||||||
|
After getting started, this section outlines the foundational [editing components](../components/overview) in BlockSuite, namely `Editor`, `Fragment`, `Block` and `Widget`.
|
||||||
|
|
||||||
|
## Editors and Fragments
|
||||||
|
|
||||||
|
The `@blocksuite/presets` package includes reusable editors like `PageEditor` and `EdgelessEditor`. Besides these editors, BlockSuite also defines **_fragments_** - UI components that are **NOT** editors but are dependent on the document's state. These fragments, such as sidebars, panels, and toolbars, may be independent in lifecycle from the editors, yet should work out-of-the-box when attached to the block tree.
|
||||||
|
|
||||||
|
The distinction between editors and fragments lies in their complexity and functionality. **Fragments typically offer more simplified capabilities, serving specific UI purposes, whereas editors provide comprehensive editing capabilities over the block tree**. Nevertheless, both editors and fragments shares similar [data flows](/blog/crdt-native-data-flow).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Blocks and Widgets
|
||||||
|
|
||||||
|
To address the complexity and diversity of editing needs, BlockSuite architects its editors as assemblies of multiple editable blocks, termed [`BlockSpec`](./block-spec)s. Each block spec encapsulates the data schema, view, service, and logic required to compose the editor. These block specs collectively define the editable components within the editor's environment.
|
||||||
|
|
||||||
|
Within each block spec, there can be [`Widget`](./block-widgets)s specific to that block's implementation, enhancing interactivity within the editor. BlockSuite leverages this widget mechanism to register dynamic UI components such as drag handles and slash menus within the page editor.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Composing Editors by Blocks
|
||||||
|
|
||||||
|
In BlockSuite, the `editor` is typically designed to be remarkably lightweight. The actual editable blocks are registered to the [`EditorHost`](/api/@blocksuite/block-std/) component, which is a container for mounting block UI components.
|
||||||
|
|
||||||
|
BlockSuite by default offers a host based on the [lit](https://lit.dev) framework. For example, this is a conceptually usable BlockSuite editor composed of [`BlockSpec`](./block-spec)s:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Default BlockSuite editable blocks
|
||||||
|
import { PageEditorBlockSpecs } from '@blocksuite/blocks';
|
||||||
|
// The container for mounting block UI components
|
||||||
|
import { EditorHost } from '@blocksuite/lit';
|
||||||
|
// The store for working with block tree
|
||||||
|
import { type Doc } from '@blocksuite/store';
|
||||||
|
|
||||||
|
// Standard lit framework primitives
|
||||||
|
import { html, LitElement } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElement('simple-page-editor')
|
||||||
|
export class SimplePageEditor extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
doc!: Doc;
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html` <editor-host .doc=${this.doc} .specs=${PageEditorBlockSpecs}></editor-host> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In other words, you can think of the BlockSuite editor as being composed in this way:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Editor = BlockSpec[];
|
||||||
|
```
|
||||||
|
|
||||||
|
With very little overhead.
|
||||||
|
|
||||||
|
So, as long as there is a corresponding `host` implementation, you can use the component model of frameworks like react or vue to implement your BlockSuite editors:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Explore the [`PageEditor` source code](https://github.com/toeverything/blocksuite/blob/master/packages/presets/src/editors/page-editor.ts) to see how this pattern allows composing minimal real-world editors.
|
||||||
|
|
||||||
|
## One Block, Multiple Specs
|
||||||
|
|
||||||
|
BlockSuite encourages the derivation of various block spec implementations from a single block model to enrich the editing experience. For instance, the root node of the block tree, the _root block_, is implemented differently for `PageEditor` and `EdgelessEditor` through two different specs **but with the same shared `RootBlockModel`**. The two block specs serve as the top-level UI components for their respective editors:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This allows you to **implement various editors easily on top of the same document**, providing diverse editing experiences and great potentials in customizability.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
So far, we have explored the interplay between different BlockSuite component types. The subsequent sections will delve deeper into the detailed framework functionalities, beginning with block tree manipulation. For the moment, understanding the structured outline of the [BlockSuite components](../components/overview) gallery might provide clearer insights.
|
||||||
101
blocksuite/docs-site/guide/data-synchronization.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Data Synchronization
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🌐 This documentation has a [Chinese translation](https://insider.affine.pro/share/af3478a2-9c9c-4d16-864d-bffa1eb10eb6/xiObHbAC0yUb7HmX4-fjg).
|
||||||
|
:::
|
||||||
|
|
||||||
|
This guide explores several optimal ways to synchronize (in other words, save and load) documents in BlockSuite.
|
||||||
|
|
||||||
|
## Snapshot API
|
||||||
|
|
||||||
|
Traditionally, you might expect a JSON-based API that works somewhat like `editor.load()`. For such scenarios, BlockSuite indeed conveniently fulfills this need through its built-in snapshot mechanism:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Job } from '@blocksuite/store';
|
||||||
|
|
||||||
|
const { collection } = doc;
|
||||||
|
|
||||||
|
// A job is required for performing the tasks
|
||||||
|
const job = new Job({ collection });
|
||||||
|
|
||||||
|
// Export current doc content to snapshot JSON
|
||||||
|
const json = await job.docToSnapshot(doc);
|
||||||
|
|
||||||
|
// Import snapshot JSON to a new doc
|
||||||
|
const newDoc = await job.snapshotToDoc(json);
|
||||||
|
```
|
||||||
|
|
||||||
|
The snapshot stores the JSON representation of the `doc` block tree, preserving its nested structure. Additionally, BlockSuite has designed an [Adapter](./adapter) API on top of the snapshot to handle conversions between the block tree and third-party formats like markdown and HTML.
|
||||||
|
|
||||||
|
## Document Streaming
|
||||||
|
|
||||||
|
Different from the classic mechanism above, BlockSuite natively supports a state management strategy that can be mentally paralleled with [React Server Components](https://www.joshwcomeau.com/react/server-components/). This allows the state of the block tree to be directly used as serializable data, streaming from the server (or local database) to the client.
|
||||||
|
|
||||||
|
In this case, **the document data stored on the server is no longer JSON, but always a binary representation of CRDT** (similar to protobuf or RSC payload). As the block tree in BlockSuite is natively implemented by CRDT, and the CRDT data is always updated first during state updates ([see this article](/blog/crdt-native-data-flow)), the block tree state in the BlockSuite editor is always driven entirely by CRDT data. Therefore, compared to the RSC mindset:
|
||||||
|
|
||||||
|
```
|
||||||
|
ui = f(data)(state)
|
||||||
|
```
|
||||||
|
|
||||||
|
The BlockSuite mindset is always:
|
||||||
|
|
||||||
|
```
|
||||||
|
ui = f(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is equivalent to updating the server first every time you update a todo list item, and then updating the state with the data returned from the server. However, with the ability of CRDT that automatically resolves conflicts, this process can be reliably completed locally and synchronized with remote documents.
|
||||||
|
|
||||||
|
In contrast, traditional editors typically only support APIs like `editor.load()`, which is more similar to a compromised `f(data)(state)` model, and has more complexity when dealing with real-time collaboration with multiple data sources.
|
||||||
|
|
||||||
|
In BlockSuite, the data-driven synchronization strategy is implemented through providers:
|
||||||
|
|
||||||
|
- When creating a new document, you only need to connect the `doc` to a specific provider (or multiple providers) to expect the CRDT data of the block tree to be synchronized via these providers.
|
||||||
|
- Similarly, when loading an existing document, the method is to create a new empty `doc` object and connect it to the corresponding provider. At this time, the block tree data will also flow in from the provider data source:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { AffineSchemas } from '@blocksuite/blocks';
|
||||||
|
import { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
|
import { Schema } from '@blocksuite/store';
|
||||||
|
import { DocCollection, Text } from '@blocksuite/store';
|
||||||
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
|
||||||
|
const schema = new Schema().register(AffineSchemas);
|
||||||
|
const collection = new DocCollection({ schema });
|
||||||
|
collection.meta.initialize();
|
||||||
|
|
||||||
|
// Let's start with an empty doc
|
||||||
|
const doc = collection.createDoc();
|
||||||
|
const editor = new AffineEditorContainer();
|
||||||
|
editor.doc = doc;
|
||||||
|
document.body.append(editor);
|
||||||
|
|
||||||
|
// Case 1.
|
||||||
|
// If you are creating a new doc,
|
||||||
|
// init in this way and the blocks will be automatically written to IndexedDB
|
||||||
|
function createDoc() {
|
||||||
|
new IndexeddbPersistence('provider-demo', doc.spaceDoc);
|
||||||
|
|
||||||
|
doc.load(() => {
|
||||||
|
const pageBlockId = doc.addBlock('affine:page', {
|
||||||
|
title: new Text('Test'),
|
||||||
|
});
|
||||||
|
doc.addBlock('affine:surface', {}, pageBlockId);
|
||||||
|
const noteId = doc.addBlock('affine:note', {}, pageBlockId);
|
||||||
|
doc.addBlock('affine:paragraph', { text: new Text('Hello World!') }, noteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2.
|
||||||
|
// If you are loading an existing doc,
|
||||||
|
// simply load content using the provider callback
|
||||||
|
function loadDoc() {
|
||||||
|
const provider = new IndexeddbPersistence('provider-demo', doc.spaceDoc);
|
||||||
|
provider.on('synced', () => doc.load());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Furthermore, by connecting multiple providers, documents can automatically be synchronized to a variety of different backends:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As an example, when testing real-time collabortion in BlockSuite [following the steps](https://github.com/toeverything/blocksuite/blob/master/BUILDING.md#test-collaboration), all the clients would connect to the WebSocket provider. The first client should enter case 1, and the clients joining the room afterwards would run into case 2.
|
||||||
140
blocksuite/docs-site/guide/event.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Event
|
||||||
|
|
||||||
|
This document introduces the handling of UI events, event flows within the block tree, and the implementation of hotkeys in BlockSuite.
|
||||||
|
|
||||||
|
## Handling UI Events
|
||||||
|
|
||||||
|
For UI events such as `click` in editor, there is an underlying event dispatcher in `@blocksuite/block-std` to dispatch events. With the dispatcher, you can handle events in your [block view](./block-view) implementation in this manner:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@customElements('my-block')
|
||||||
|
class MyBlockView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _handleClick = () => {
|
||||||
|
//...
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Bubbling
|
||||||
|
|
||||||
|
All events on dispatcher are bound to the root element of the block view to make it possible to bubble events to the parent block view.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class ChildView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _handleClick = () => {
|
||||||
|
console.log('click1');
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _handleClick = () => {
|
||||||
|
console.log('click2');
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When you click on the `ChildView`, the console will print:
|
||||||
|
|
||||||
|
```
|
||||||
|
click1
|
||||||
|
click2
|
||||||
|
```
|
||||||
|
|
||||||
|
You may want to stop the event from bubbling to the parent block view. You can simply return `true` in the event handler:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class ChildView extends BlockComponent<MyBlockModel> {
|
||||||
|
private _handleClick = () => {
|
||||||
|
console.log('click1');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the console will only print:
|
||||||
|
|
||||||
|
```
|
||||||
|
click1
|
||||||
|
```
|
||||||
|
|
||||||
|
The event bubbling is implemented by event target. For the events that won't support bubbling, the event dispatcher will use [block path](#) to dispatch events to the parent block views.
|
||||||
|
|
||||||
|
### Event Scope
|
||||||
|
|
||||||
|
By default, `handleEvents` will only subscribe events triggered by the block view and its children.
|
||||||
|
We also provide two more scopes to subscribe to make it possible to handle events triggered by other blocks.
|
||||||
|
|
||||||
|
#### Flavour Scope
|
||||||
|
|
||||||
|
The flavour scope will subscribe to events triggered by the block view and other blocks with the same flavour.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyBlock extends BlockComponent<MyBlockModel> {
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick, { flavour: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Global Scope
|
||||||
|
|
||||||
|
The global scope will subscribe to events triggered by the block view and all other blocks.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyBlock extends BlockComponent<MyBlockModel> {
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.handleEvent('click', this._handleClick, { global: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling Hotkeys
|
||||||
|
|
||||||
|
The hotkey is a special event that can be triggered by the keyboard.
|
||||||
|
|
||||||
|
Key names may be strings like `"Shift-Ctrl-Enter"`—a key identifier prefixed with zero or more modifiers. Key identifiers
|
||||||
|
are based on the strings that can appear in [`KeyEvent.key`](https:///developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key).
|
||||||
|
|
||||||
|
Use lowercase letters to refer to letter keys (or uppercase letters if you want shift to be held). You may use `"Space"` as an alias for the `" "` name.
|
||||||
|
|
||||||
|
Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or
|
||||||
|
`Meta-`) are recognized.
|
||||||
|
For characters that are created by holding shift, the `Shift-` prefix is implied, and should not be added explicitly.
|
||||||
|
|
||||||
|
You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on other platforms.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class MyBlock extends BlockComponent<MyBlockModel> {
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.bindHotkey({
|
||||||
|
'Mod-b': () => {},
|
||||||
|
'Alt-Space': () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same as `handleEvent`, you can return `true` in the hotkey handler to stop the event from bubbling to the parent block view.
|
||||||
114
blocksuite/docs-site/guide/inline.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# `@blocksuite/inline`
|
||||||
|
|
||||||
|
This package is a minimal rich text component for inline editing. It uses an external [`Y.Text`](https://docs.yjs.dev/api/shared-types/y.text) as it source of truth. Every `inlineEditor` instance attaches to an independant `Y.Text`, so rich text content in different block nodes can be splitted into different inline editors, making complex content conveniently composable. This significantly reduces the complexity required to implement traditional rich text editing features.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can use `InlineEditor` without other BlockSuite dependencies:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { InlineEditor } from '@blocksuite/inline';
|
||||||
|
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const yText = doc.getText('text');
|
||||||
|
const inlineEditor = new InlineEditor(yText);
|
||||||
|
|
||||||
|
const myEditor = document.getElementById('my-editor');
|
||||||
|
inlineEditor.mount(myEditor);
|
||||||
|
```
|
||||||
|
|
||||||
|
The [inline editor playground](https://try-blocksuite.vercel.app/examples/inline/)
|
||||||
|
is used for online testing and you can also check out the [source code](https://github.com/toeverything/blocksuite/tree/master/packages/playground/examples/inline) in its repository.
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
Attributes is the property of [delta](https://quilljs.com/docs/delta/) structure, which is used to store formatting information.
|
||||||
|
|
||||||
|
A delta expressing a bold text node in this manner:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"insert": "Hello World",
|
||||||
|
"attributes": {
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The inline editor use [zod](https://zod.dev/) to validate attributes, you can use the `inlineEditor.setAttributesSchema` to set the schema:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Generally you don't have to extend `baseTextAttributes`
|
||||||
|
const customSchema = baseTextAttributes.extend({
|
||||||
|
reference: z
|
||||||
|
.object({
|
||||||
|
type: type: z.enum([
|
||||||
|
'LinkedPage',
|
||||||
|
]),
|
||||||
|
pageId: z.string(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.catch(undefined),
|
||||||
|
background: z.string().optional().nullable().catch(undefined),
|
||||||
|
color: z.string().optional().nullable().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const yText = doc.getText('text');
|
||||||
|
const inlineEditor = new InlineEditor(yText);
|
||||||
|
inlineEditor.setAttributesSchema(customSchema);
|
||||||
|
|
||||||
|
const editorContainer = document.getElementById('editor');
|
||||||
|
inlineEditor.mount(editorContainer);
|
||||||
|
```
|
||||||
|
|
||||||
|
`InlineEditor` has default attributes schema, so you can skip this step if you think it is enough.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Default attributes schema
|
||||||
|
const baseTextAttributes = z.object({
|
||||||
|
bold: z.literal(true).optional().nullable().catch(undefined),
|
||||||
|
italic: z.literal(true).optional().nullable().catch(undefined),
|
||||||
|
underline: z.literal(true).optional().nullable().catch(undefined),
|
||||||
|
strike: z.literal(true).optional().nullable().catch(undefined),
|
||||||
|
code: z.literal(true).optional().nullable().catch(undefined),
|
||||||
|
link: z.string().optional().nullable().catch(undefined),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes Renderer
|
||||||
|
|
||||||
|
Attributes Renderer is a function that takes a delta and returns `TemplateResult<1>`, which is a valid [lit-html](https://github.com/lit/lit/tree/main/packages/lit-html) template result.
|
||||||
|
|
||||||
|
`InlineEditor` use this function to render text with custom format and it is also the way to customize the text render.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type AffineTextAttributes = {
|
||||||
|
// Your custom attributes
|
||||||
|
};
|
||||||
|
|
||||||
|
const attributeRenderer: AttributeRenderer<AffineTextAttributes> = (
|
||||||
|
delta,
|
||||||
|
// You can use `selected` to check if the text node is selected
|
||||||
|
selected
|
||||||
|
) => {
|
||||||
|
// Generate style from delta
|
||||||
|
return html`<span style=${style}><v-text .str=${delta.insert}></v-text></span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const yText = doc.getText('text');
|
||||||
|
const inlineEditor = new InlineEditor(yText);
|
||||||
|
inlineEditor.setAttributeRenderer(attributeRenderer);
|
||||||
|
|
||||||
|
const editorContainer = document.getElementById('editor');
|
||||||
|
inlineEditor.mount(editorContainer);
|
||||||
|
```
|
||||||
|
|
||||||
|
You will see there is a `v-text` in the template, it is a custom element that render text node. `InlineEditor` use them to calculate range so you have to use them to render text content from delta.
|
||||||
|
|
||||||
|
## Rich Text Component
|
||||||
|
|
||||||
|
If you find the `InlineEditor` features may be limited or a bit verbose to use, you can refer to or directly use the [rich-text](https://github.com/toeverything/blocksuite/blob/f71df00ce18e3f300caad914aaedf63267158885/packages/blocks/src/components/rich-text/rich-text.ts) encapsulated in the `@blocksuite/blocks` package. It contains basic editing features like copy/cut/paste, undo/redo (including range restore).
|
||||||
96
blocksuite/docs-site/guide/overview.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# BlockSuite Framework 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](../components/overview) for building various editors, based on a minimized vanilla framework as their runtime. With BlockSuite, you can:
|
||||||
|
|
||||||
|
- Reuse multiple first-party BlockSuite editors:
|
||||||
|
- [**`PageEditor`**](../components/editors/page-editor): A comprehensive block-based document editor, offering extensive customization and flexibility.
|
||||||
|
- [**`EdgelessEditor`**](../components/editors/edgeless-editor): A graphics editor with opt-in canvas rendering support, but also shares the same rich-text capabilities with the `PageEditor`.
|
||||||
|
- Customize, extend and enhance these editors with a rich set of [BlockSuite components](../components/overview) and [examples](https://github.com/toeverything/blocksuite/tree/master/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 vallina 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](https://github.com/toeverything/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](../blog/crdt-native-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.
|
||||||
|
|
||||||
|
<!-- ## Examples -->
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
With BlockSuite editors, you can selectively reuse all the editing features in [AFFiNE](https://affine.pro/):
|
||||||
|
|
||||||
|
[](https://affine.pro)
|
||||||
|
|
||||||
|
And under the hood, the vanilla BlockSuite framework supports:
|
||||||
|
|
||||||
|
- Defining [custom blocks](./working-with-block-tree#defining-new-blocks) and inline embeds.
|
||||||
|
- Incremental updates, [real-time collaboration](https://github.com/toeverything/blocksuite/blob/master/BUILDING.md#test-collaboration), and even decentralized data synchronization based on the [document streaming](./data-synchronization#document-streaming) mechanism.
|
||||||
|
- Writing type-safe complex editing logic based on the [command](./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](./data-synchronization#snapshot-api) and transformer.
|
||||||
|
- State scheduling across multiple documents and reusing one document in multiple editors.
|
||||||
|
|
||||||
|
To try out BlockSuite, refer to the [quick start](./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](https://github.com/microsoft/monaco-editor) and [VSCode](https://code.visualstudio.com/), 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_components).
|
||||||
|
|
||||||
|
This difference has led BlockSuite to set clear boundaries between packages, 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](https://github.com/toeverything/blocksuite/tree/master/packages/framework) and prebuilt editing components.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Framework</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@blocksuite/store</code></td>
|
||||||
|
<td>Data layer for modeling collaborative document states. It is natively built on the CRDT library <a href="https://github.com/yjs/yjs">Yjs</a>, powering all BlockSuite documents with built-in real-time collaboration and time-travel capabilities.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@blocksuite/inline</code></td>
|
||||||
|
<td>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. <strong>This significantly reduces the complexity required to implement traditional rich text editing features.</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@blocksuite/block-std</code></td>
|
||||||
|
<td>Framework-agnostic library for modeling editable blocks. Its capabilities cover the structure of block fields, events, selection, clipboard support, etc.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Components</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@blocksuite/blocks</code></td>
|
||||||
|
<td>Default block implementations for composing preset editors, including widgets belonging to each block.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@blocksuite/presets</code></td>
|
||||||
|
<td>Plug-and-play editable components including <i>editors</i> (<code>PageEditor</code> / <code>EdgelessEditor</code>) and auxiliary UI components named <i>fragments</i> (<code>CopilotPanel</code>, <code>DocTitle</code>...).</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
116
blocksuite/docs-site/guide/quick-start.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
For a swift start with BlockSuite, you can either kick off with ready-made examples for popular frameworks, or simply install the core packages to integrate it into your project.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
If this is your first time using BlockSuite, referring to the [overview](./overview) section may be helpful.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Bootstrap Project
|
||||||
|
|
||||||
|
BlockSuite works with all common frameworks, you can start from these examples that basically builds a TodoMVC-like note app based on BlockSuite.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Framework</th>
|
||||||
|
<th>Link</th>
|
||||||
|
<th>Maintaining</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="TypeScript" />Vanilla</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/vanilla-indexeddb" target="_blank">vanilla-indexeddb</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Next" />Next</td>
|
||||||
|
<td><a href="https://github.com/toeverything/blocksuite-examples/tree/master/react-basic-next" target="_blank">react-basic-next</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="React" />React</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/react-basic" target="_blank">react-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Vue" />Vue</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/vue-basic" target="_blank">vue-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Angular" />Angular</td>
|
||||||
|
<td><a href="https://github.com/toeverything/blocksuite-examples/tree/master/angular-basic" target="_blank">angular-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Preact" icon="https://raw.githubusercontent.com/preactjs/preact-www/master/src/assets/branding/symbol.svg" />Preact</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/preact-basic" target="_blank">preact-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Svelte" />Svelte</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/svelte-basic" target="_blank">svelte-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" />Solid</td>
|
||||||
|
<td><a href="https://stackblitz.com/github/toeverything/blocksuite-examples/tree/master/solid-basic" target="_blank">solid-basic</a></td>
|
||||||
|
<td>✅</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Init From Scratch
|
||||||
|
|
||||||
|
To use BlockSuite in your existing project, simply install these core packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn install \
|
||||||
|
@blocksuite/presets@canary \
|
||||||
|
@blocksuite/blocks@canary \
|
||||||
|
@blocksuite/store@canary
|
||||||
|
```
|
||||||
|
|
||||||
|
Key takeaways in the snippet above:
|
||||||
|
|
||||||
|
- The `@blocksuite/presets` package contains the prebuilt editors and opt-in additional UI components.
|
||||||
|
- To work with the BlockSuite document model and first-party blocks, the `@blocksuite/store` and `@blocksuite/blocks` packages are required.
|
||||||
|
- The BlockSuite `canary` versions are released daily based on the master branch, which is also used in production in [AFFiNE](https://github.com/toeverything/AFFiNE).
|
||||||
|
|
||||||
|
Then you can use the prebuilt `PageEditor` out of the box, with an initialized `doc` instance attached as its document model:
|
||||||
|
|
||||||
|
::: code-sandbox {coderHeight=420 previewHeight=300}
|
||||||
|
|
||||||
|
```ts /index.ts [active]
|
||||||
|
import { createEmptyDoc, PageEditor } from '@blocksuite/presets';
|
||||||
|
import { Text } from '@blocksuite/store';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Init editor with default block tree
|
||||||
|
const doc = createEmptyDoc().init();
|
||||||
|
const editor = new PageEditor();
|
||||||
|
editor.doc = doc;
|
||||||
|
document.body.appendChild(editor);
|
||||||
|
|
||||||
|
// Update block node with some initial text content
|
||||||
|
const paragraphs = doc.getBlockByFlavour('affine:paragraph');
|
||||||
|
const paragraph = paragraphs[0];
|
||||||
|
doc.updateBlock(paragraph, { text: new Text('Hello World!') });
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The `PageEditor` here is a standard web component that can also be reused with `<page-editor>` HTML tag. Another `EdgelessEditor` also works similarly - simply attach the `editor` with a `doc` and you are all set.
|
||||||
|
|
||||||
|
For the `doc.getBlockByFlavour` and `doc.updateBlock` APIs used here, please see the [introduction](./working-with-block-tree#block-tree-basics) about block tree basics for further details.
|
||||||
|
|
||||||
|
As the next step, you can choose to:
|
||||||
|
|
||||||
|
- Explore how BlockSuite break down editors into different [component types](./component-types). Taking a look at the list of [BlockSuite components](../components/overview) may also be helpful.
|
||||||
|
- Try collaborative editing [following the steps](https://github.com/toeverything/blocksuite/blob/master/BUILDING.md#test-collaboration).
|
||||||
|
- Learn about [basic concepts](./working-with-block-tree) in BlockSuite framework that are used throughout the development of editors.
|
||||||
|
|
||||||
|
Note that BlockSuite is still under rapid development. For any questions or feedback, feel free to let us know!
|
||||||
205
blocksuite/docs-site/guide/selection.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 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](/blog/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:
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
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'],
|
||||||
|
});
|
||||||
|
```
|
||||||
42
blocksuite/docs-site/guide/slot.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Slot
|
||||||
|
|
||||||
|
BlockSuite extensively uses `Slot` to manage events that are not DOM-native. You can think of it as a type-safe event emitter or a simplified RxJS [Observable](https://rxjs.dev/guide/observable):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Slot } from '@blocksuite/store';
|
||||||
|
|
||||||
|
// Create a new slot
|
||||||
|
const slot = new Slot<{ name: string }>();
|
||||||
|
|
||||||
|
// Subscribe events
|
||||||
|
slot.on(({ name }) => console.log(name));
|
||||||
|
|
||||||
|
// Or alternatively only listen event once
|
||||||
|
slot.once(({ name }) => console.log(name));
|
||||||
|
|
||||||
|
// Emit the event
|
||||||
|
slot.emit({ name: 'foo' });
|
||||||
|
```
|
||||||
|
|
||||||
|
To unsubscribe from the slot, simply use the return value of `slot.on()`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const slot = new Slot();
|
||||||
|
const disposable = slot.on(myHandler);
|
||||||
|
|
||||||
|
// Dispose the subscription
|
||||||
|
disposable.dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
Moreover, for any node in the block tree, events can be triggered when the node is updated:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const model = doc.root[0];
|
||||||
|
|
||||||
|
// Triggered when the `props` of the block model is updated
|
||||||
|
model.propsUpdated.on(() => updateMyComponent());
|
||||||
|
// Triggered when the `children` of the block model is updated
|
||||||
|
model.childrenUpdated.on(() => updateMyComponent());
|
||||||
|
```
|
||||||
|
|
||||||
|
In the prebuilt AFFiNE editor, which is based on the [lit](https://lit.dev/) framework, the UI component of each block subscribes to its model updates using this pattern.
|
||||||
62
blocksuite/docs-site/guide/store.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# `@blocksuite/store`
|
||||||
|
|
||||||
|
This package is the data layer for modeling collaborative document states. It's natively built on the CRDT library [Yjs](https://github.com/yjs/yjs), powering all BlockSuite documents with built-in real-time collaboration and time-travel capabilities.
|
||||||
|
|
||||||
|
## `Doc`
|
||||||
|
|
||||||
|
In BlockSuite, a [`Doc`](/api/@blocksuite/store/classes/Doc.html) is the container for a block tree, providing essential functionalities for creating, retrieving, updating, and deleting blocks inside it. Under the hood, every doc holds a Yjs [subdocument](https://docs.yjs.dev/api/subdocuments).
|
||||||
|
|
||||||
|
Besides the block tree, the [selection](./selection) state is also stored in the [`doc.awarenessStore`](/api/@blocksuite/store/classes/Doc.html#awarenessstore) inside the doc. This store is also built on top of the Yjs [awareness](https://docs.yjs.dev/api/about-awareness).
|
||||||
|
|
||||||
|
## `DocCollection`
|
||||||
|
|
||||||
|
In BlockSuite, a [`DocCollection`](/api/@blocksuite/store/classes/DocCollection.html) is defined as an opt-in collection of multiple docs, providing comprehensive features for managing cross-doc updates and data synchronization. You can access the collection via the `doc.collection` getter, or you can also create a collection manually:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { DocCollection, Schema } from '@blocksuite/store';
|
||||||
|
|
||||||
|
const schema = new Schema();
|
||||||
|
|
||||||
|
// You can register a batch of block schemas to the collection
|
||||||
|
schema.register(AffineSchemas);
|
||||||
|
|
||||||
|
const collection = new DocCollection({ schema });
|
||||||
|
collection.meta.initialize();
|
||||||
|
```
|
||||||
|
|
||||||
|
Then multiple `doc`s can be created under the collection:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const collection = new DocCollection({ schema });
|
||||||
|
collection.meta.initialize();
|
||||||
|
|
||||||
|
// This is an empty doc at this moment
|
||||||
|
const doc = collection.createDoc();
|
||||||
|
```
|
||||||
|
|
||||||
|
As an example, the `createEmptyDoc` is a simple helper implemented exactly in this way ([source](https://github.com/toeverything/blocksuite/blob/master/packages/presets/src/helpers/index.ts)):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
|
import { Schema, DocCollection } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export function createEmptyDoc() {
|
||||||
|
const schema = new Schema().register(AffineSchemas);
|
||||||
|
const collection = new DocCollection({ schema });
|
||||||
|
collection.meta.initialize();
|
||||||
|
const doc = collection.createDoc();
|
||||||
|
|
||||||
|
return {
|
||||||
|
doc,
|
||||||
|
async init() {
|
||||||
|
await doc.load(() => {
|
||||||
|
const rootBlockId = doc.addBlock('affine:page', {});
|
||||||
|
doc.addBlock('affine:surface', {}, rootBlockId);
|
||||||
|
const noteId = doc.addBlock('affine:note', {}, rootBlockId);
|
||||||
|
doc.addBlock('affine:paragraph', {}, noteId);
|
||||||
|
});
|
||||||
|
return doc;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
354
blocksuite/docs-site/guide/working-with-block-tree.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# Working with Block Tree
|
||||||
|
|
||||||
|
::: info
|
||||||
|
🌐 This documentation has a [Chinese translation](https://insider.affine.pro/share/af3478a2-9c9c-4d16-864d-bffa1eb10eb6/-3bEQPBoOEkNH13ULW9Ed).
|
||||||
|
:::
|
||||||
|
|
||||||
|
In previous examples, we demonstrated how a `doc` collaborates with an `editor`. In this document, we will introduce the basic structure of the block tree within the `doc` and the common methods for controlling it in an editor environment.
|
||||||
|
|
||||||
|
## Block Tree Basics
|
||||||
|
|
||||||
|
In BlockSuite, each `doc` object manages an independent block tree composed of various types of blocks. These blocks can be defined through the [`BlockSchema`](./block-schema.md), 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](https://github.com/toeverything/AFFiNE) project, the default editable blocks use the `affine` prefix.
|
||||||
|
|
||||||
|
To manipulate blocks, you can utilize several primary APIs under `doc`:
|
||||||
|
|
||||||
|
- [`doc.addBlock`](/api/@blocksuite/store/classes/Doc.html#addblock)
|
||||||
|
- [`doc.updateBlock`](/api/@blocksuite/store/classes/Doc.html#updateblock)
|
||||||
|
- [`doc.deleteBlock`](/api/@blocksuite/store/classes/Doc.html#deleteblock)
|
||||||
|
- [`doc.getBlockById`](/api/@blocksuite/store/classes/Doc.html#getblockbyid)
|
||||||
|
|
||||||
|
Here is an example demonstrating the manipulation of the block tree through these APIs:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// The first block will be added as root
|
||||||
|
const rootId = doc.addBlock('affine:page');
|
||||||
|
|
||||||
|
// Insert second block as a child of the root with empty props
|
||||||
|
const props = {};
|
||||||
|
const noteId = doc.addBlock('affine:note', props, rootId);
|
||||||
|
|
||||||
|
// You can also provide an optional `parentIndex`
|
||||||
|
const paragraphId = doc.addBlock('affine:paragraph', props, noteId, 0);
|
||||||
|
|
||||||
|
const modelA = doc.root!.children[0].children[0];
|
||||||
|
const modelB = doc.getBlockById(paragraphId);
|
||||||
|
console.log(modelA === modelB); // true
|
||||||
|
|
||||||
|
// Update the paragraph type to 'h1'
|
||||||
|
doc.updateBlock(modelA, { type: 'h1' });
|
||||||
|
|
||||||
|
doc.deleteBlock(modelA);
|
||||||
|
```
|
||||||
|
|
||||||
|
This example creates a subset of the block tree hierarchy defaultly used in `@blocksuite/presets`, illustrated as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In BlockSuite, you need to initialize a valid document structure before attaching it to editors, which is also why it requires `init()` after `createEmptyDoc()`.
|
||||||
|
|
||||||
|
::: 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 `doc` are automatically recorded and can be reversed using [`doc.undo()`](/api/@blocksuite/store/classes/Doc.html#undo) and [`doc.redo()`](/api/@blocksuite/store/classes/Doc.html#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 [`doc.captureSync()`](/api/@blocksuite/store/classes/Doc.html#capturesync) between block operations:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const rootId = doc.addBlock('affine:page');
|
||||||
|
const noteId = doc.addBlock('affine:note', props, rootId);
|
||||||
|
|
||||||
|
// Capture a history record now
|
||||||
|
doc.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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
|
||||||
|
- As [mentioned before](./component-types#composing-editors-by-blocks), the `editor.host` - also known as the [`EditorHost`](/api/@blocksuite/block-std/) component, is a container for mounting block UI components. It handles the heavy lifting involved in mapping the **block tree** to the **component tree**.
|
||||||
|
- Regardless of the framework used to implement `EditorHost`, they can access the same headless standard library designed for editable blocks through `host.std`. For example, `std.spec` contains all the registered [`BlockSpec`](./block-spec)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
|
||||||
|
|
||||||
|
The essence of editor lies in allowing users to **dynamically select and modify** the data. In BlockSuite, you can use the `SelectionManager`, which is responsible for managing selections, through `std.selection` or `host.selection`. As an example, after selecting some blocks in the editor, you can execute the following code snippets line by line in the console:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Get current selection state
|
||||||
|
const cached = selection.value;
|
||||||
|
|
||||||
|
// Clear current selection state
|
||||||
|
selection.clear();
|
||||||
|
|
||||||
|
// Recover the selection state from cache
|
||||||
|
selection.set(cached);
|
||||||
|
|
||||||
|
// Try setting only part of the selection
|
||||||
|
selection.set([cached[0]]);
|
||||||
|
```
|
||||||
|
|
||||||
|
In `block-std`, BlockSuite implements several atomic selection types for `SelectionManager`, such as `TextSelection` and `BlockSelection`. The content currently selected by the user is automatically divided into these primitive selection data structures, recorded in the list returned by `selection.value`. Through `selection.set()`, you can also programmatically control the current selection state of the editor.
|
||||||
|
|
||||||
|
This allows the selection manager to handle different types of selections, as shown in the following illustration, using the same API:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In `selection.value`, different types of selection states can coexist simultaneously. Each selection object records at least the `id` and `path` of the corresponding selected block (i.e., the sequence of ids of all blocks from the root block to that block). Moreover, you can further categorize different types of selections using the `group` field. For example in `PageEditor`, both `TextSelection` and `BlockSelection` belong to the `note` group. Hence, the example structure of block selection in the above image is as follows:
|
||||||
|
|
||||||
|
```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'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
For the more complex native [selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection), the `TextSelection` can be used to model it. It marks the start and end positions of the native selection in the block through the `from` and `to` fields, recording only the `index` and `length` of the inline text sequence in the respective block. This simplification is made possible by the architecture of BlockSuite, where editable blocks use `@blocksuite/inline` as the rich text editing component. Each block tree node's rich text content is rendered independently into different inline editors, eliminating nesting between rich text instances:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Additionally, the entire `selection.value` object is isolated under the `clientId` scope of the current session. During collaborative editing, selection instances between different clients will be distributed in real-time (via [providers](./data-synchronization#document-streaming)), facilitating the implementation of UI states like remote cursors.
|
||||||
|
|
||||||
|
For more advanced usage and details, please refer to the [`Selection`](./selection) documentation.
|
||||||
|
|
||||||
|
## Service and Commands
|
||||||
|
|
||||||
|
In many cases, operations on the block tree within an editor environment need further encapsulation. For example, when using the selection manager mentioned before, since the atomic selection state only includes `id`s, retrieving the corresponding block model based on `selection.value` often requires some boilerplate, as follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function getFirstSelectedModel(host: EditorHost) {
|
||||||
|
const { selection, doc } = host;
|
||||||
|
const firstSelection = selection.value[0];
|
||||||
|
const { path } = firstSelection;
|
||||||
|
const leafId = path[path.length - 1];
|
||||||
|
const blockModel = doc.getBlockById(leafId);
|
||||||
|
return blockModel;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This direct usage is not very convenient. Also, as BlockSuite encourages completely splitting the editor into different [`BlockSpec`](./block-spec)s ([recall here](./component-types#composing-editors-by-blocks)), which indicates that methods and properties globally available in the editor should also be implemented on the block level. A mechanism is needed at this point to organize such code, ensuring maintainability in large projects. This is why BlockSuite introduces the concept of [`BlockService`](./block-service).
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
In BlockSuite, service is used for registering state or methods specific to a certain block type. For example, instead of implementing the `getFirstSelectedModel` method yourself, you can use shortcuts predefined on `RootService`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const rootService = host.spec.getService('affine:page');
|
||||||
|
|
||||||
|
// Get models of selected blocks
|
||||||
|
rootService.selectedModel;
|
||||||
|
// Get UI components of selected blocks
|
||||||
|
rootService.selectedBlocks;
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `getService` is used to obtain the service corresponding to a certain block spec. Each service is a plain class, existing as a singleton throughout the lifecycle of the `host` (with a corresponding [`mounted`](/api/@blocksuite/block-std/classes/BlockService.html#mounted) lifecycle hook). Some typical uses of service include:
|
||||||
|
|
||||||
|
- For blocks that serve as the root node of the block tree, common editor APIs can be registered on their services for application developers.
|
||||||
|
- For blocks requiring specific dynamic configurations, service can be used to pass in corresponding options. For example, a service can accept configurations related to image uploading for image block.
|
||||||
|
- For blocks that need to execute certain side effects (such as subscribing to keyboard shortcuts) when the editor loads, operations on `host` can be done in the `mounted` callback of their services. In this way, even if the block does not yet exist in the block tree, the corresponding logic will still execute.
|
||||||
|
|
||||||
|
As an example, the following code more specifically shows how the two getters `selectedBlocks` and `selectedModels` mentioned earlier are implemented using a service:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BlockService } from '@blocksuite/block-std';
|
||||||
|
import type { BlockComponent } from '@blocksuite/lit';
|
||||||
|
import type { RootBlockModel } from './root-model.js';
|
||||||
|
|
||||||
|
export class RootService extends BlockService<RootBlockModel> {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// A plain getter in service
|
||||||
|
get selectedBlocks() {
|
||||||
|
let result: BlockComponent[] = [];
|
||||||
|
// Here we are using something new...
|
||||||
|
// Introducing commands!
|
||||||
|
this.std.command
|
||||||
|
.chain()
|
||||||
|
.tryAll(chain => [chain.getTextSelection(), chain.getImageSelections(), chain.getBlockSelections()])
|
||||||
|
.getSelectedBlocks()
|
||||||
|
.inline(({ selectedBlocks }) => {
|
||||||
|
if (!selectedBlocks) return;
|
||||||
|
result = selectedBlocks;
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another plain getter
|
||||||
|
get selectedModels() {
|
||||||
|
return this.selectedBlocks.map(block => block.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Besides the service, this code snippet also utilizes the chain of commands occurring on `this.std.command` (the [`CommandManager`](/api/@blocksuite/block-std/classes/CommandManager)). This is about using predefined [`Command`](./command)s.
|
||||||
|
|
||||||
|
In BlockSuite, you can always control the editor solely through direct operations on `host` and `doc`. However, in the real world, it's often necessary to treat some operations as variables, dynamically constructing control flow (e.g., dynamically combining different subsequent processing logics based on current selection states). This is where commands really shines. **It allows complex sequences of operations to be recorded as reusable chains, and also simplifies the context sharing between operations**.
|
||||||
|
|
||||||
|
The code at the end of the previous section demonstrates the basic usage of commands:
|
||||||
|
|
||||||
|
- The `pipe` method is used to start a new command chain.
|
||||||
|
- The `tryAll` method sequentially executes multiple sub-commands on the current chain's context.
|
||||||
|
- Commands like `getSelectedBlock` and `getTextSelection` are used for actual block tree operations.
|
||||||
|
- The `inline` method transfers the state on the command context object to the outside or executes other side effects.
|
||||||
|
- The `run` method is used to finally execute the command chain. The context will be destroyed after the command chain execution.
|
||||||
|
|
||||||
|
In the above methods, task-specific commands like `getSelectedBlock` are not implemented by the command manager but are registered by individual blocks. In fact, since BlockSuite separates the framework-specific `host` from the framework-agnostic `block-std`.
|
||||||
|
|
||||||
|
You can refer to the [`Command`](./command) documentation for more advanced uses of commands.
|
||||||
|
|
||||||
|
::: info
|
||||||
|
We plan to continue supplementing and documenting some of the most commonly used commands, please stay tuned.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Defining New Blocks
|
||||||
|
|
||||||
|
So far, we have introduced almost all the main parts that make up a block spec. Now, it's time to learn how to create new block types.
|
||||||
|
|
||||||
|
In BlockSuite, the block spec is built from three main components: [`schema`](./block-schema), [`service`](./block-service), and [`view`](./block-view). Among these, the definition of the `view` part is specific to the frontend framework used. For example, in the case of `PageEditor` and `EdgelessEditor` based on `@blocksuite/lit`, the block specs are defined with lit primitives in this way:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { BlockSpec } from '@blocksuite/block-std';
|
||||||
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
const MyBlockSpec: BlockSpec = {
|
||||||
|
schema: MyBlockSchema, // Define this with `defineBlockSchema`
|
||||||
|
service: MyBlockService, // Extend `BlockService`
|
||||||
|
// Define lit components here
|
||||||
|
view: {
|
||||||
|
component: literal`my-block-component`,
|
||||||
|
widgets: {
|
||||||
|
myToolbar: literal`my-toolbar`,
|
||||||
|
myMenu: literal`my-menu`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This design aims at balancing ease of use with customizability. Both the service and view are built around the schema, which allows for different components, services, and widgets to be implemented for the same block model, enabling:
|
||||||
|
|
||||||
|
- A single block to have multiple component views. For example, `PageEditor` and `EdgelessEditor` have different implementations of the root block ([recall here](./component-types#one-block-multiple-specs)).
|
||||||
|
- A single block to be configured with different widget combinations. For instance, you can remove all widgets to compose read-only editors.
|
||||||
|
- A single block to even be implemented based on different frontend frameworks, by simply providing an `EditorHost` middleware implementation for the respective framework.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: info
|
||||||
|
|
||||||
|
- In terms of cross-framework support, the biggest difference between BlockSuite and other popular editor frameworks is that **BlockSuite has no DOM host of its own**. Instead, it implements a middleware like `@blocksuite/lit`, mapping the block tree to the framework's component tree. Thus, the entire content area of a BlockSuite editor is natively controlled by different frameworks, rather than creating many different framework component subtrees within a BlockSuite-controlled DOM tree through excessive use of `createRoot`.
|
||||||
|
- The reason BlockSuite uses lit by default is that as a web component framework, the lit component tree IS the DOM tree natively. This simplifies the three-phase update process of `block tree -> component tree -> DOM tree` to just `block tree -> component (DOM) tree`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Furthermore, BlockSuite also supports defining the most commonly used type of custom block in a more straightforward way: the _embed block_. **This type of block does not nest other blocks and manages its internal area's state entirely on its own**. For example, to create a GitHub link card that can be displayed in `PageEditor`, you can start by defining the model:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BlockModel } from '@blocksuite/store';
|
||||||
|
import { defineEmbedModel } from '@blocksuite/blocks';
|
||||||
|
|
||||||
|
// Define strongly typed block model
|
||||||
|
export class EmbedGithubModel extends defineEmbedModel<{
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}>(BlockModel) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then based on this model, a lit-based UI component for the block can be defined:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { EmbedBlockComponent } from '@blocksuite/blocks';
|
||||||
|
import type { EmbedGithubBlockModel } from './embed-github-model.js';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElement('affine-embed-github-block')
|
||||||
|
export class EmbedGithubBlock extends EmbedBlockComponent<EmbedGithubModel> {
|
||||||
|
// styles...
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return this.renderEmbed(() => {
|
||||||
|
return html`
|
||||||
|
<div class="affine-embed-github-block">
|
||||||
|
<h3>GitHub Card</h3>
|
||||||
|
<div>${this.model.owner}/${this.model.repo}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As next step, we can further define the corresponding `BlockSpec`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createEmbedBlock } from '@blocksuite/blocks';
|
||||||
|
import { EmbedGithubBlockModel } from './embed-github-model.js';
|
||||||
|
|
||||||
|
export const EmbedGithubBlockSpec = createEmbedBlock({
|
||||||
|
schema: {
|
||||||
|
name: 'github',
|
||||||
|
version: 1,
|
||||||
|
toModel: () => new EmbedGithubModel(),
|
||||||
|
props: () => ({
|
||||||
|
owner: '',
|
||||||
|
repo: '',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
component: literal`affine-embed-github-block`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, by inserting this `BlockSpec` into the `host.specs` array, you can expand with new block types:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ...
|
||||||
|
import { PageEditorBlockSpecs } from '@blocksuite/blocks';
|
||||||
|
import { EmbedGithubBlockSpec } from './embed-block-spec.js';
|
||||||
|
|
||||||
|
const editor = new PageEditor();
|
||||||
|
editor.specs = [...PageEditorBlockSpecs, EmbedGithubBlockSpec];
|
||||||
|
editor.doc = doc;
|
||||||
|
```
|
||||||
|
|
||||||
|
After completing the above steps, you can insert the new block type into the block tree:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const props = {
|
||||||
|
owner: 'toeverything', // The company behind BlockSuite and AFFiNE 🤫
|
||||||
|
repo: 'https://github.com/toeverything/blocksuite',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The 'affine' prefix is kept by default, but you can also override it.
|
||||||
|
doc.addBlock('affine:embed-github', props, parentId);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can view the [source code](https://github.com/toeverything/blocksuite/tree/master/packages/blocks/src/embed-github-block) for the above example in BlockSuite repository.
|
||||||
|
|
||||||
|
Combining the earlier example of composing `PageEditor` entirely based on block spec ([recall here](./component-types#composing-editors-by-blocks)), this should give you a more direct understanding of BlockSuite's extensibility.
|
||||||
BIN
blocksuite/docs-site/images/affine-demo.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
blocksuite/docs-site/images/attach-editors.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
blocksuite/docs-site/images/bidirectional-data-flow.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
blocksuite/docs-site/images/block-nesting.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
blocksuite/docs-site/images/block-selection-example.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
blocksuite/docs-site/images/block-spec.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
blocksuite/docs-site/images/block-std-data-flow.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
blocksuite/docs-site/images/blocksuite-cover.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
blocksuite/docs-site/images/component-types.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
blocksuite/docs-site/images/composing-editors-1.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
blocksuite/docs-site/images/composing-editors-2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
blocksuite/docs-site/images/context-interleaving.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
blocksuite/docs-site/images/crdt-native-data-flow.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
blocksuite/docs-site/images/editor-structure.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
blocksuite/docs-site/images/encoded-crdt-binary.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
blocksuite/docs-site/images/flat-inlines.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
blocksuite/docs-site/images/framework-agnostic.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
blocksuite/docs-site/images/hello-blocksuite.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
blocksuite/docs-site/images/inline-example.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
blocksuite/docs-site/images/package-overview.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
blocksuite/docs-site/images/pluggable-providers.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
blocksuite/docs-site/images/selection-types.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
blocksuite/docs-site/images/showcase-edgeless-perf.jpg
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
blocksuite/docs-site/images/showcase-fragments-1.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
blocksuite/docs-site/images/showcase-fragments-2.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
blocksuite/docs-site/images/showcase-page-edgeless-editors.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
blocksuite/docs-site/images/text-selection-example.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
33
blocksuite/docs-site/index.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
title: BlockSuite
|
||||||
|
titleTemplate: Content Editing Tech Stack
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: BlockSuite
|
||||||
|
text: Content Editing Tech Stack for the Web
|
||||||
|
tagline: BlockSuite is a toolkit for building editors and collaborative applications.
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Get Started
|
||||||
|
link: /guide/quick-start
|
||||||
|
- theme: alt
|
||||||
|
text: Learn More
|
||||||
|
link: /guide/overview
|
||||||
|
|
||||||
|
features:
|
||||||
|
- title: 🧩 Headless Editor Framework
|
||||||
|
details: BlockSuite provides a vanilla framework for building various editors, enabling the design of diverse editing interfaces.
|
||||||
|
link: /guide/overview
|
||||||
|
linkText: Learn More
|
||||||
|
- title: 🎨 Extensive Components
|
||||||
|
details: Based on the framework, BlockSuite ships components for building complex editor UIs, which are highly interoperable.
|
||||||
|
link: /components/overview
|
||||||
|
linkText: Learn More
|
||||||
|
- title: 🧬 Collaborative at Core
|
||||||
|
details: Natively powered by CRDT, BlockSuite supports document streaming and conflict resolution at its heart, ready for collaboration inherently.
|
||||||
|
link: /blog/crdt-native-data-flow
|
||||||
|
linkText: Learn More
|
||||||
|
---
|
||||||
32
blocksuite/docs-site/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@blocksuite/docs",
|
||||||
|
"description": "BlockSuite documentation",
|
||||||
|
"private": true,
|
||||||
|
"keywords": [],
|
||||||
|
"author": "toeverything",
|
||||||
|
"repository": "toeverything/blocksuite",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"typedoc": "typedoc --options ./typedoc.json",
|
||||||
|
"dev": "yarn run typedoc && yarn exec vitepress dev --port 5200",
|
||||||
|
"dev:nobuild": "yarn exec vitepress dev --port 5200",
|
||||||
|
"build": "yarn run typedoc && NODE_OPTIONS=--max-old-space-size=8192 yarn exec vitepress build",
|
||||||
|
"preview": "yarn exec vitepress preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@blocksuite/affine": "workspace:*",
|
||||||
|
"date-fns": "^4.0.0",
|
||||||
|
"markdown-it-container": "^4.0.0",
|
||||||
|
"vitepress-plugin-sandpack": "^1.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/markdown-it-container": "^4.0.0",
|
||||||
|
"typedoc": "^0.28.0",
|
||||||
|
"typedoc-plugin-markdown": "^4.5.0",
|
||||||
|
"vite-plugin-wasm": "^3.3.0",
|
||||||
|
"vitepress": "^1.6.3",
|
||||||
|
"vue": "^3.4.38"
|
||||||
|
},
|
||||||
|
"version": "0.26.3"
|
||||||
|
}
|
||||||
1
blocksuite/docs-site/public/_redirects
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/blocksuite-overview.html /guide/overview.html 301
|
||||||
5
blocksuite/docs-site/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["./.vitepress"],
|
||||||
|
"references": [{ "path": "../affine/all" }]
|
||||||
|
}
|
||||||
32
blocksuite/docs-site/typedoc.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"entryPoints": ["../**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"../docs",
|
||||||
|
"../docs-site",
|
||||||
|
"../integration-test",
|
||||||
|
"../playground",
|
||||||
|
"**/__tests__/**/*"
|
||||||
|
],
|
||||||
|
"packageOptions": {
|
||||||
|
"includeVersion": true,
|
||||||
|
"readme": "none",
|
||||||
|
"excludeExternals": true,
|
||||||
|
"externalPattern": ["node_modules/**/*"],
|
||||||
|
"entryPoints": ["src/index.ts"]
|
||||||
|
},
|
||||||
|
"plugin": ["typedoc-plugin-markdown"],
|
||||||
|
"out": "./api",
|
||||||
|
"entryPointStrategy": "packages",
|
||||||
|
"includeVersion": false,
|
||||||
|
"logLevel": "Error",
|
||||||
|
"readme": "none",
|
||||||
|
"name": "BlockSuite API Documentation",
|
||||||
|
"entryFileName": "index.md",
|
||||||
|
"outputFileStrategy": "members",
|
||||||
|
"hidePageHeader": true,
|
||||||
|
"excludePrivate": true,
|
||||||
|
"excludeProtected": true,
|
||||||
|
"excludeExternals": true,
|
||||||
|
"categorizeByGroup": true,
|
||||||
|
"sort": ["source-order"]
|
||||||
|
}
|
||||||
2
blocksuite/docs-site/wrangler.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
name = "blocksuite-docs"
|
||||||
|
pages_build_output_dir = ".vitepress/dist"
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
"typedarray": "npm:@nolyfill/typedarray@^1",
|
"typedarray": "npm:@nolyfill/typedarray@^1",
|
||||||
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
|
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
|
||||||
"fs-xattr": "npm:@napi-rs/xattr@latest",
|
"fs-xattr": "npm:@napi-rs/xattr@latest",
|
||||||
|
"ioredis": "5.8.2",
|
||||||
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch",
|
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch",
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch",
|
"@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch",
|
||||||
"yjs": "patch:yjs@npm%3A13.6.21#~/.yarn/patches/yjs-npm-13.6.21-c9f1f3397c.patch"
|
"yjs": "patch:yjs@npm%3A13.6.21#~/.yarn/patches/yjs-npm-13.6.21-c9f1f3397c.patch"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"@queuedash/api": "^3.16.0",
|
"@queuedash/api": "^3.16.0",
|
||||||
"@react-email/components": "^0.5.7",
|
"@react-email/components": "^0.5.7",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"bullmq": "^5.40.2",
|
"bullmq": "5.53.0",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"eventemitter2": "^6.4.9",
|
"eventemitter2": "^6.4.9",
|
||||||
"exa-js": "^2.4.0",
|
"exa-js": "^2.4.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"fast-xml-parser": "^5.5.7",
|
"fast-xml-parser": "^5.7.2",
|
||||||
"get-stream": "^9.0.1",
|
"get-stream": "^9.0.1",
|
||||||
"google-auth-library": "^10.2.0",
|
"google-auth-library": "^10.2.0",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.13.2",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import './config';
|
|||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { DynamicModule } from '@nestjs/common';
|
import { DynamicModule } from '@nestjs/common';
|
||||||
import { type QueueOptions } from 'bullmq';
|
import { type QueueOptions } from 'bullmq';
|
||||||
|
import { type Redis as IORedis } from 'ioredis';
|
||||||
|
|
||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
import { QueueRedis } from '../../redis';
|
import { QueueRedis } from '../../redis';
|
||||||
@@ -31,7 +32,7 @@ export class JobModule {
|
|||||||
// to avoid new jobs been dropped by old deployments
|
// to avoid new jobs been dropped by old deployments
|
||||||
prefix,
|
prefix,
|
||||||
defaultJobOptions: config.job.queue,
|
defaultJobOptions: config.job.queue,
|
||||||
connection: redis,
|
connection: redis as IORedis,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: [Config, QueueRedis],
|
inject: [Config, QueueRedis],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"fast-xml-parser": "^5.5.7",
|
"fast-xml-parser": "^5.7.2",
|
||||||
"s3mini": "^0.9.1"
|
"s3mini": "^0.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
20
packages/common/y-octo/utils/fuzz/Cargo.lock
generated
@@ -459,7 +459,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -545,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
"rand 0.8.5",
|
"rand 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -611,9 +611,9 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
@@ -622,9 +622,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
@@ -675,7 +675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1177,7 +1177,7 @@ dependencies = [
|
|||||||
"nanoid",
|
"nanoid",
|
||||||
"nom",
|
"nom",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1192,7 +1192,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"lib0",
|
"lib0",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"y-octo",
|
"y-octo",
|
||||||
"y-octo-utils",
|
"y-octo-utils",
|
||||||
@@ -1207,7 +1207,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"lib0",
|
"lib0",
|
||||||
"phf",
|
"phf",
|
||||||
"rand 0.9.3",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"y-octo",
|
"y-octo",
|
||||||
"yrs",
|
"yrs",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"description": "Generate changelog from version changes",
|
"description": "Generate changelog from version changes",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/simple-git": "^0.1.22",
|
"@napi-rs/simple-git": "^0.1.22",
|
||||||
"@slack/web-api": "^7.8.0",
|
"@slack/web-api": "^7.15.1",
|
||||||
"changelogithub": "^13.0.0",
|
"changelogithub": "^13.0.0",
|
||||||
"jsx-slack": "^6.1.2",
|
"jsx-slack": "^6.1.2",
|
||||||
"marked": "^15.0.12"
|
"marked": "^15.0.12"
|
||||||
|
|||||||