Compare commits

..

35 Commits

Author SHA1 Message Date
EYHN
b427a89c9a fix(core): fix awareness send message repeatedly (#10643) 2025-03-10 12:26:57 +08:00
forehalo
00398fc63a fix(server): reschedule busy doc merging 2025-03-03 18:38:05 +08:00
Saul-Mirone
cbef681125 fix: table collab (#10489)
[Screen Recording 2025-02-27 at 20.24.15.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/EUhyG5TOGVlHZ0Suk1wH/b3ac681f-a14d-483a-820c-53c584f0fb44.mov" />](https://app.graphite.dev/media/video/EUhyG5TOGVlHZ0Suk1wH/b3ac681f-a14d-483a-820c-53c584f0fb44.mov)
2025-03-03 18:09:29 +08:00
forehalo
bf42a4ddb2 fix(server): limit max batch pulled doc updates 2025-03-03 17:02:48 +08:00
Saul-Mirone
d57ef5c5b3 fix(editor): transform to draftmodel first when get snapshot (#10477) 2025-02-27 07:52:18 +00:00
darkskygit
1fd3d618be feat(server): update search model (#10475)
fix AF-2283
2025-02-27 07:24:53 +00:00
donteatfriedrice
7c8ba13aad fix(core): extract a scrollable text renderer fot ai panel (#10469) 2025-02-27 07:00:16 +00:00
liuyi
b3821ad619 fix(server): avoid global rejection when event handler errors (#10467) 2025-02-27 06:25:46 +00:00
fundon
caa4dfedfc fix(editor): adjust black and white in shape text color palettes to pure black and pure white (#10450)
Closes: [BS-2697](https://linear.app/affine-design/issue/BS-2697/检查shape-text-color黑白不映射的pr合并状态)

https://github.com/user-attachments/assets/732612e9-5e43-453f-aef2-5f32f5a08614
2025-02-27 06:05:42 +00:00
zzj3720
18dfad28d7 fix(editor): toDraftModal supports flat data structures (#10466)
fix: PD-2374
fix: BS-2703
2025-02-27 05:16:10 +00:00
akumatus
f43a848e18 feat(core): convert ai think tag to markdown divider (#10459)
Support issue [AF-2282](https://linear.app/affine-design/issue/AF-2282).
2025-02-27 04:43:11 +00:00
doodlewind
8cec22cc64 fix(editor): handle resize in turbo renderer (#10465) 2025-02-27 04:18:37 +00:00
pengx17
be94f3fc17 fix(native): potential sharablecontent icon/name crash (#10464) 2025-02-27 03:19:52 +00:00
Yifeng Wang
e9484e8e15 refactor(editor): remove non null asserts in turbo renderer (#10454) 2025-02-27 10:47:26 +08:00
pengx17
f25266ec88 fix(editor): ai chat panel textarea selection issue (#10461)
fix AF-2244
2025-02-27 02:26:57 +00:00
doodlewind
3252dd7a31 feat(editor): automatically hide canvas optimized blocks (#10451)
Qualified DOM blocks can now be optimized away automatically.

<img alt="image.png" width="500" src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/102bf813-154a-4816-9eb0-2c9c0ce01fe7.png">

Since this is under development, verifying state correctness is more important than rendering details.

This won't affect current production version since the passive APIs added to `GfxViewportElement` are disabled by the `enableOptimization = false` config.
2025-02-27 02:10:49 +00:00
akumatus
903d260880 fix(core): ai chat panel scrolling dizziness problem (#10458)
Fix issue [AF-2281](https://linear.app/affine-design/issue/AF-2281).

### What Changed?
- During the re-rendering process of the rich-text editor, the container height is always expanded.
- If the user manually scrolls the chat panel, immediately stop automatically scrolling

[录屏2025-02-27 07.30.08.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/624ea4fa-b8dd-4cf2-a9be-6997bdabc97b.mov" />](https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/624ea4fa-b8dd-4cf2-a9be-6997bdabc97b.mov)
2025-02-26 23:59:12 +00:00
Saul-Mirone
2c79d7229f refactor(editor): remove legacy service watcher (#10455)
The main changes in this PR involve replacing the deprecated `BlockServiceWatcher` with the new `LifeCycleWatcher` across multiple files. Here's a detailed breakdown:

1. **Core Architectural Change:**
   - Removed `BlockServiceWatcher` class completely (deleted file)
   - Migrated to `LifeCycleWatcher` as the new standard for watching component lifecycle events

2. **Key Changes in Implementation:**
   - Changed from using `blockService.specSlots` events to using `view.viewUpdated` events
   - Replaced `flavour` static property with `key` static property
   - Updated event handling to use more specific payload type checking

3. **Major File Changes:**
   - Modified multiple block components:
     - Embed synced doc block
     - Frame preview
     - Edgeless root spec
     - AI-related components (code, image, paragraph, etc.)
     - Quick search service
     - Edgeless clipboard

4. **Pattern of Changes:**
   The migration follows a consistent pattern:
   ```typescript
   // Old pattern
   class SomeWatcher extends BlockServiceWatcher {
     static override readonly flavour = 'some:flavour';
     mounted() {
       this.blockService.specSlots.viewConnected.on(...)
     }
   }

   // New pattern
   class SomeWatcher extends LifeCycleWatcher {
     static override key = 'some-watcher';
     mounted() {
       const { view } = this.std;
       view.viewUpdated.on(payload => {
         if (payload.type !== 'block' || payload.method !== 'add') return;
         // Handle event
       });
     }
   }
   ```

5. **Benefits:**
   - More explicit and type-safe event handling
   - Cleaner architecture by removing deprecated code
   - More consistent approach to lifecycle management
   - Better separation of concerns

This appears to be a significant architectural improvement that modernizes the codebase by removing deprecated patterns and standardizing on a more robust lifecycle management system.
2025-02-26 15:15:45 +00:00
Saul-Mirone
fd6d96a38e refactor(editor): use transformer from store when possible (#10453) 2025-02-26 14:15:04 +00:00
Saul-Mirone
1c5e360d7e feat(editor): add widget in viewUpdated slot (#10452) 2025-02-26 13:24:32 +00:00
doodlewind
589622043c fix(editor): list toggle position offset (#10448)
Before:

![image](https://github.com/user-attachments/assets/fbddc396-1078-4c1e-8e2c-c26c6e4a6d61)

After:

<img width="579" alt="image" src="https://github.com/user-attachments/assets/7b049b53-269b-484e-ba76-fa6f46a2004c" />

This centering approach won't affect heading blocks:

<img width="267" alt="image" src="https://github.com/user-attachments/assets/3f3217e3-7e23-43fc-a7e5-33b6eadccc88" />
2025-02-26 12:09:00 +00:00
Saul-Mirone
ce87dcf58e feat(editor): schema extension (#10447)
1. **Major Architectural Change: Schema Management**
   - Moved from `workspace.schema` to `store.schema` throughout the codebase
   - Removed schema property from Workspace and Doc interfaces
   - Added `BlockSchemaExtension` pattern across multiple block types

2. **Block Schema Extensions Added**
   - Added new `BlockSchemaExtension` to numerous block types including:
     - DataView, Surface, Attachment, Bookmark, Code
     - Database, Divider, EdgelessText, Embed blocks (Figma, Github, HTML, etc.)
     - Frame, Image, Latex, List, Note, Paragraph
     - Root, Surface Reference, Table blocks

3. **Import/Export System Updates**
   - Updated import functions to accept `schema` parameter:
     - `importHTMLToDoc`
     - `importHTMLZip`
     - `importMarkdownToDoc`
     - `importMarkdownZip`
     - `importNotionZip`
   - Modified export functions to use new schema pattern

4. **Test Infrastructure Updates**
   - Updated test files to use new schema extensions
   - Modified test document creation to include schema extensions
   - Removed direct schema registration in favor of extensions

5. **Service Layer Changes**
   - Updated various services to use `getAFFiNEWorkspaceSchema()`
   - Modified transformer initialization to use document schema
   - Updated collection initialization patterns

6. **Version Management**
   - Removed version-related properties and methods from:
     - `WorkspaceMetaImpl`
     - `TestMeta`
     - `DocImpl`
   - Removed `blockVersions` and `workspaceVersion/pageVersion`

7. **Store and Extension Updates**
   - Added new store extensions and adapters
   - Updated store initialization patterns
   - Added new schema-related functionality in store extension

This PR represents a significant architectural shift in how schemas are managed, moving from a workspace-centric to a store-centric approach, while introducing a more extensible block schema system through `BlockSchemaExtension`. The changes touch multiple layers of the application including core functionality, services, testing infrastructure, and import/export capabilities.
2025-02-26 11:31:29 +00:00
L-Sun
2732b96d00 fix(editor): overflow of embed github card in edgeless note (#10442)
This PR fixes the overflow of the `embed-github-card` inside edgeless notes.

https://github.com/user-attachments/assets/21775d0f-e4c8-4fcc-86d8-aafb27033358
2025-02-26 09:33:57 +00:00
doodlewind
0f8c837fbe refactor(editor): simplify renderer state (#10441)
The redundant `monitoring` state has been removed. The new `ready` state is used for querying if DOM elements can be safely optimized away.
2025-02-26 08:39:58 +00:00
fundon
c058f94e15 chore(editor): improve color formatting tests (#10429) 2025-02-26 08:19:54 +00:00
darkskygit
d25b216311 feat(server): adapt doc loader for server native (#9942) 2025-02-26 08:05:20 +00:00
CatsJuice
e1fd8f5d80 fix(core): correctly toggle visibility of starter-bar based on doc.isEmpty (#10439) 2025-02-26 07:49:51 +00:00
fundon
866b096304 fix(core): fix doc url parsing with custom domain names (#10444)
Closes: [AF-2279](https://affine-pro.slack.com/archives/C06CTBH5L4R/p1740552397245649?thread_ts=1740547457.278239&cid=C06CTBH5L4R)
2025-02-26 07:35:25 +00:00
doodlewind
e38e59d4e5 refactor(editor): request refresh after finding stale bitmap (#10438)
This ensures a bitmap will be eventually generated after tile got invalidated.
2025-02-26 06:49:09 +00:00
EYHN
7dbc1e300d fix(ios): fix magic link sign in (#10436) 2025-02-26 06:32:16 +00:00
zzj3720
1a9bfeaa2c fix(editor): table block supports parsing rich text (#10430)
fix: BS-2685
2025-02-26 04:56:55 +00:00
doodlewind
97cc814a22 fix(editor): remote cursor color inconsistency (#10437)
Fixed:

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/c2db021e-6ee9-4022-913c-84b84953d36e.png)
2025-02-26 04:42:05 +00:00
donteatfriedrice
d63f16da5e fix(editor): affine preview root style (#10420)
Fix [BS-2677](https://linear.app/affine-design/issue/BS-2677/linked-doc-embed-view样式错误)

1. Only show the border of embed synced doc block (in note) when hover.
2. Fix affine preview root padding style, set padding only when affine preview root in embed synced doc block (in surface).
3. Only add the footnote config extension to the chat panel and chat block center peek. For footnotes in other page preview scenarios, such as footnote nodes within embed synced doc blocks or embed linked doc blocks, the hover effect should be maintained.
2025-02-26 04:25:24 +00:00
zzj3720
0e4a79959f fix(editor): improve string conversion logic for checkbox property (#10433)
fix: BS-2465

- Add a FALSE_VALUES set containing various falsy string representations

- Support Chinese negation terms like "否", "不", "错", etc.

- Optimize the implementation of cellFromString method
2025-02-25 23:23:00 +00:00
doodlewind
f3911b1b5e fix(editor): discard stale layout bitmap in turbo renderer (#10427)
Fixes this bug caused by stale bitmap:

[Screen Recording 2025-02-24 at 6.10.19 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/lEGcysB4lFTEbCwZ8jMv/3e24f4b7-6f95-4c7c-a79a-b8e4ffdb3b10.mov" />](https://app.graphite.dev/media/video/lEGcysB4lFTEbCwZ8jMv/3e24f4b7-6f95-4c7c-a79a-b8e4ffdb3b10.mov)
2025-02-25 10:51:55 +00:00
209 changed files with 2508 additions and 1501 deletions

7
Cargo.lock generated
View File

@@ -69,6 +69,7 @@ dependencies = [
"core-foundation",
"coreaudio-rs",
"dispatch2",
"libc",
"napi",
"napi-build",
"napi-derive",
@@ -3110,7 +3111,6 @@ version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
@@ -3912,14 +3912,13 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.1"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a802c93485fb6781d27e27cb5927f6b00ff8d26b56c70af87267be7e99def97"
checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.5",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
]

View File

@@ -24,6 +24,7 @@ dispatch2 = "0.2"
dotenvy = "0.15"
file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3"
libc = "0.2"
mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }

View File

@@ -5,7 +5,11 @@ import {
type InsertToPosition,
} from '@blocksuite/affine-shared/utils';
import type { DataViewDataType } from '@blocksuite/data-view';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
type Props = {
title: string;
@@ -93,3 +97,6 @@ export const DataViewBlockSchema = defineBlockSchema({
return new DataViewBlockModel();
},
});
export const DataViewBlockSchemaExtension =
BlockSchemaExtension(DataViewBlockSchema);

View File

@@ -9,6 +9,7 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { findAncestorModel } from '@blocksuite/affine-shared/utils';
import type { BlockService } from '@blocksuite/block-std';
import type { GfxCompatibleProps } from '@blocksuite/block-std/gfx';
import type { BlockModel } from '@blocksuite/store';
@@ -57,7 +58,15 @@ export class EmbedBlockComponent<
) {
this.style.display = 'block';
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
const insideNote = findAncestorModel(
this.model,
m => m.flavour === 'affine:note'
);
if (
!insideNote &&
this.std.get(DocModeProvider).getEditorMode() === 'edgeless'
) {
this.style.minWidth = `${EMBED_CARD_MIN_WIDTH}px`;
}
}

View File

@@ -2,6 +2,7 @@ import { css, html } from 'lit';
export const styles = css`
.affine-embed-github-block {
container: affine-embed-github-block / inline-size;
box-sizing: border-box;
display: flex;
width: 100%;
@@ -24,6 +25,7 @@ export const styles = css`
padding: 12px;
border-radius: var(--1, 0px);
opacity: var(--add, 1);
overflow: hidden;
}
.affine-embed-github-content-title {
@@ -376,6 +378,15 @@ export const styles = css`
display: none;
}
}
@container affine-embed-github-block (width < 375px) {
.affine-embed-github-content {
width: 100%;
}
.affine-embed-github-banner {
display: none;
}
}
`;
export const GithubIcon = html`<svg

View File

@@ -26,9 +26,9 @@ import {
} from '@blocksuite/affine-shared/utils';
import {
BlockSelection,
BlockServiceWatcher,
BlockStdScope,
type EditorHost,
LifeCycleWatcher,
} from '@blocksuite/block-std';
import {
GfxControllerIdentifier,
@@ -124,27 +124,31 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
this.std.getOptional(EditorSettingProvider) ??
signal(GeneralSettingSchema.parse({}));
class EmbedSyncedDocWatcher extends BlockServiceWatcher {
static override readonly flavour = 'affine:embed-synced-doc';
class EmbedSyncedDocWatcher extends LifeCycleWatcher {
static override key = 'embed-synced-doc-watcher';
override mounted() {
const disposableGroup = this.blockService.disposables;
const slots = this.blockService.specSlots;
disposableGroup.add(
slots.viewConnected.on(({ component }) => {
const nextComponent = component as EmbedSyncedDocBlockComponent;
override mounted(): void {
const { view } = this.std;
view.viewUpdated.on(payload => {
if (
payload.type !== 'block' ||
payload.view.model.flavour !== 'affine:embed-synced-doc'
) {
return;
}
const nextComponent = payload.view as EmbedSyncedDocBlockComponent;
if (payload.method === 'add') {
nextComponent.depth = nextDepth;
currentDisposables.add(() => {
nextComponent.depth = 0;
});
})
);
disposableGroup.add(
slots.viewDisconnected.on(({ component }) => {
const nextComponent = component as EmbedSyncedDocBlockComponent;
return;
}
if (payload.method === 'delete') {
nextComponent.depth = 0;
})
);
return;
}
});
}
}
@@ -231,6 +235,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
[theme]: true,
surface: false,
selected: this.selected$.value,
'show-hover-border': true,
})}
@click=${this._handleClick}
style=${containerStyleMap}

View File

@@ -57,10 +57,13 @@ export const blockStyles = css`
}
.affine-embed-synced-doc-container {
border: 1px solid var(--affine-border-color);
border: 1px solid transparent;
border-radius: 8px;
overflow: hidden;
}
.affine-embed-synced-doc-container.show-hover-border:hover {
border-color: var(--affine-border-color);
}
.affine-embed-synced-doc-container.page {
display: block;
width: 100%;
@@ -151,7 +154,12 @@ export const blockStyles = css`
}
.affine-embed-synced-doc-container.surface {
border-color: var(--affine-border-color);
background: var(--affine-background-primary-color);
affine-preview-root {
padding: 0 24px;
}
}
.affine-embed-synced-doc-container

View File

@@ -64,7 +64,6 @@ import {
BlockSnapshotSchema,
fromJSON,
type SliceSnapshot,
Transformer,
} from '@blocksuite/store';
import DOMPurify from 'dompurify';
import * as Y from 'yjs';
@@ -373,15 +372,7 @@ export class EdgelessClipboardController extends PageClipboard {
if (mayBeSurfaceDataJson !== undefined) {
const elementsRawData = JSON.parse(mayBeSurfaceDataJson);
const { snapshot, blobs } = elementsRawData;
const job = new Transformer({
schema: this.std.workspace.schema,
blobCRUD: this.std.workspace.blobSync,
docCRUD: {
create: (id: string) => this.std.workspace.createDoc({ id }),
get: (id: string) => this.std.workspace.getDoc(id),
delete: (id: string) => this.std.workspace.removeDoc(id),
},
});
const job = this.std.store.getTransformer();
const map = job.assetsManager.getAssets();
decodeClipboardBlobs(blobs, map);
for (const blobId of map.keys()) {
@@ -1377,15 +1368,7 @@ export async function prepareClipboardData(
selectedAll: GfxModel[],
std: BlockStdScope
) {
const job = new Transformer({
schema: std.workspace.schema,
blobCRUD: std.workspace.blobSync,
docCRUD: {
create: (id: string) => std.workspace.createDoc({ id }),
get: (id: string) => std.workspace.getDoc(id),
delete: (id: string) => std.workspace.removeDoc(id),
},
});
const job = std.store.getTransformer();
const selected = await Promise.all(
selectedAll.map(async selected => {
const data = serializeElement(selected, selectedAll, job);

View File

@@ -1,9 +1,9 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import {
BlockServiceWatcher,
BlockStdScope,
type EditorHost,
LifeCycleWatcher,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
@@ -117,22 +117,26 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
private _initSpec() {
const refreshViewport = this._refreshViewport.bind(this);
class FramePreviewWatcher extends BlockServiceWatcher {
static override readonly flavour = 'affine:page';
class FramePreviewWatcher extends LifeCycleWatcher {
static override key = 'frame-preview-watcher';
override mounted() {
const blockService = this.blockService;
blockService.disposables.add(
blockService.specSlots.viewConnected.on(({ component }) => {
const edgelessBlock =
component as EdgelessRootPreviewBlockComponent;
edgelessBlock.editorViewportSelector = 'frame-preview-viewport';
edgelessBlock.service.viewport.sizeUpdated.once(() => {
refreshViewport();
});
})
);
const { view } = this.std;
view.viewUpdated.on(payload => {
if (
payload.type !== 'block' ||
payload.method !== 'add' ||
payload.view.model.flavour !== 'affine:page'
) {
return;
}
const edgelessBlock =
payload.view as EdgelessRootPreviewBlockComponent;
edgelessBlock.editorViewportSelector = 'frame-preview-viewport';
edgelessBlock.service.viewport.sizeUpdated.once(() => {
refreshViewport();
});
});
}
}
this._previewSpec.extend([FramePreviewWatcher]);

View File

@@ -1,5 +1,9 @@
import type { Color, ColorScheme, Palette } from '@blocksuite/affine-model';
import { isTransparent, resolveColor } from '@blocksuite/affine-model';
import {
DefaultTheme,
isTransparent,
resolveColor,
} from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ColorEvent } from '@blocksuite/affine-shared/utils';
import { css, html, LitElement, nothing, svg, type TemplateResult } from 'lit';
@@ -253,7 +257,7 @@ export class EdgelessColorPanel extends LitElement {
accessor openColorPicker!: (e: MouseEvent) => void;
@property({ type: Array })
accessor palettes: readonly Palette[] = [];
accessor palettes: readonly Palette[] = DefaultTheme.Palettes;
@property({ attribute: false })
accessor theme!: ColorScheme;

View File

@@ -1,8 +1,4 @@
import {
type ColorScheme,
DefaultTheme,
type StrokeStyle,
} from '@blocksuite/affine-model';
import { type ColorScheme, type StrokeStyle } from '@blocksuite/affine-model';
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, LitElement } from 'lit';
@@ -44,7 +40,6 @@ export class StrokeStylePanel extends WithDisposable(LitElement) {
aria-label="Border colors"
.value=${this.strokeColor}
.theme=${this.theme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${this.hollowCircle}
@select=${(e: ColorEvent) => this.setStrokeColor(e)}
>

View File

@@ -65,7 +65,7 @@ export class EdgelessBrushMenu extends EdgelessToolbarToolMixin(
class="one-way"
.value=${this._props$.value.color}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorPalettes}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}

View File

@@ -133,7 +133,7 @@ export class EdgelessConnectorMenu extends EdgelessToolbarToolMixin(
class="one-way"
.value=${stroke}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorPalettes}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}

View File

@@ -75,9 +75,10 @@ export class EdgelessShapeMenu extends SignalWatcher(
const filled = !isTransparent(value);
const fillColor = value;
const strokeColor = filled
? DefaultTheme.StrokeColorPalettes.find(palette => palette.key === key)
?.value
: DefaultTheme.StrokeColorMap.Grey;
? DefaultTheme.StrokeColorShortPalettes.find(
palette => palette.key === key
)?.value
: DefaultTheme.StrokeColorShortMap.Grey;
const { shapeName } = this._props$.value;
this.edgeless.std
@@ -173,7 +174,7 @@ export class EdgelessShapeMenu extends SignalWatcher(
class="one-way"
.value=${fillColor}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.FillColorPalettes}
.palettes=${DefaultTheme.FillColorShortPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}

View File

@@ -33,7 +33,7 @@ export class EdgelessTextMenu extends EdgelessToolbarToolMixin(LitElement) {
class="one-way"
.value=${this.color}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorPalettes}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
@select=${(e: ColorEvent) => this.onChange({ color: e.detail })}
></edgeless-color-panel>
</div>

View File

@@ -2,11 +2,14 @@ import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-conne
import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title';
import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
import {
BlockServiceWatcher,
BlockViewExtension,
LifeCycleWatcher,
WidgetViewExtension,
} from '@blocksuite/block-std';
import { ToolController } from '@blocksuite/block-std/gfx';
import {
GfxControllerIdentifier,
ToolController,
} from '@blocksuite/block-std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -56,17 +59,12 @@ export const edgelessToolbarWidget = WidgetViewExtension(
literal`${unsafeStatic(EDGELESS_TOOLBAR_WIDGET)}`
);
class EdgelessLocker extends BlockServiceWatcher {
static override readonly flavour = 'affine:page';
class EdgelessLocker extends LifeCycleWatcher {
static override key = 'edgeless-locker';
override mounted() {
const service = this.blockService;
service.disposables.add(
service.specSlots.viewConnected.on(({ service }) => {
// Does not allow the user to move and zoom.
(service as EdgelessRootService).locked = true;
})
);
const { viewport } = this.std.get(GfxControllerIdentifier);
viewport.locked = true;
}
}

View File

@@ -16,7 +16,7 @@ import {
type DocSnapshot,
DocSnapshotSchema,
type SnapshotNode,
Transformer,
type Transformer,
} from '@blocksuite/store';
import type * as Y from 'yjs';
/**
@@ -90,16 +90,7 @@ export class TemplateJob {
type: TemplateType;
constructor({ model, type, middlewares }: TemplateJobConfig) {
this.job = new Transformer({
schema: model.doc.workspace.schema,
blobCRUD: model.doc.workspace.blobSync,
docCRUD: {
create: (id: string) => model.doc.workspace.createDoc({ id }),
get: (id: string) => model.doc.workspace.getDoc(id),
delete: (id: string) => model.doc.workspace.removeDoc(id),
},
middlewares: [],
});
this.job = model.doc.getTransformer();
this.model = model;
this.type = TEMPLATE_TYPES.includes(type as TemplateType)
? (type as TemplateType)
@@ -320,8 +311,7 @@ export class TemplateJob {
from: Record<string, Record<string, unknown>>,
to: Y.Map<Y.Map<unknown>>
) {
const schema =
this.model.doc.workspace.schema.flavourSchemaMap.get('affine:surface');
const schema = this.model.doc.schema.get('affine:surface');
const surfaceTransformer = schema?.transformer?.(
new Map()
) as SurfaceBlockTransformer;

View File

@@ -19,7 +19,7 @@ import {
isGfxGroupCompatibleModel,
type SerializedElement,
} from '@blocksuite/block-std/gfx';
import { type BlockSnapshot, Transformer } from '@blocksuite/store';
import type { BlockSnapshot, Transformer } from '@blocksuite/store';
/**
* return all elements in the tree of the elements
@@ -40,15 +40,7 @@ export function getSortedCloneElements(elements: GfxModel[]) {
export function prepareCloneData(elements: GfxModel[], std: BlockStdScope) {
elements = sortEdgelessElements(elements);
const job = new Transformer({
schema: std.workspace.schema,
blobCRUD: std.workspace.blobSync,
docCRUD: {
create: (id: string) => std.workspace.createDoc({ id }),
get: (id: string) => std.workspace.getDoc(id),
delete: (id: string) => std.workspace.removeDoc(id),
},
});
const job = std.store.getTransformer();
const res = elements.map(element => {
const data = serializeElement(element, elements, job);
return data;

View File

@@ -10,7 +10,6 @@ export class PreviewRootBlockComponent extends BlockComponent {
static override styles = css`
affine-preview-root {
display: block;
padding: 0 24px;
}
`;

View File

@@ -8,19 +8,21 @@ import {
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { Container } from '@blocksuite/global/di';
import { sha } from '@blocksuite/global/utils';
import type { Store, Workspace } from '@blocksuite/store';
import type { Schema, Store, Workspace } from '@blocksuite/store';
import { extMimeMap, Transformer } from '@blocksuite/store';
import { createAssetsArchive, download, Unzip } from './utils.js';
type ImportHTMLToDocOptions = {
collection: Workspace;
schema: Schema;
html: string;
fileName?: string;
};
type ImportHTMLZipOptions = {
collection: Workspace;
schema: Schema;
imported: Blob;
};
@@ -41,19 +43,10 @@ function getProvider() {
*/
async function exportDoc(doc: Store) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.workspace.createDoc({ id }),
get: (id: string) => doc.workspace.getDoc(id),
delete: (id: string) => doc.workspace.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(doc.workspace.id),
titleMiddleware(doc.workspace.meta.docMetas),
],
});
const job = doc.getTransformer([
docLinkBaseURLMiddleware(doc.workspace.id),
titleMiddleware(doc.workspace.meta.docMetas),
]);
const snapshot = job.docToSnapshot(doc);
const adapter = new HtmlAdapter(job, provider);
if (!snapshot) {
@@ -87,18 +80,20 @@ async function exportDoc(doc: Store) {
*
* @param options - The import options.
* @param options.collection - The target doc collection.
* @param options.schema - The schema of the target doc collection.
* @param options.html - The HTML content to import.
* @param options.fileName - Optional filename for the imported doc.
* @returns A Promise that resolves to the ID of the newly created doc, or undefined if import fails.
*/
async function importHTMLToDoc({
collection,
schema,
html,
fileName,
}: ImportHTMLToDocOptions) {
const provider = getProvider();
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
@@ -127,10 +122,15 @@ async function importHTMLToDoc({
*
* @param options - The import options.
* @param options.collection - The target doc collection.
* @param options.schema - The schema of the target doc collection.
* @param options.imported - The zip file as a Blob.
* @returns A Promise that resolves to an array of IDs of the newly created docs.
*/
async function importHTMLZip({ collection, imported }: ImportHTMLZipOptions) {
async function importHTMLZip({
collection,
schema,
imported,
}: ImportHTMLZipOptions) {
const provider = getProvider();
const unzip = new Unzip();
await unzip.load(imported);
@@ -161,7 +161,7 @@ async function importHTMLZip({ collection, imported }: ImportHTMLZipOptions) {
htmlBlobs.map(async ([fileName, blob]) => {
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),

View File

@@ -9,7 +9,7 @@ import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { Container } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists, sha } from '@blocksuite/global/utils';
import type { Store, Workspace } from '@blocksuite/store';
import type { Schema, Store, Workspace } from '@blocksuite/store';
import { extMimeMap, Transformer } from '@blocksuite/store';
import { createAssetsArchive, download, Unzip } from './utils.js';
@@ -31,12 +31,14 @@ type ImportMarkdownToBlockOptions = {
type ImportMarkdownToDocOptions = {
collection: Workspace;
schema: Schema;
markdown: string;
fileName?: string;
};
type ImportMarkdownZipOptions = {
collection: Workspace;
schema: Schema;
imported: Blob;
};
@@ -47,19 +49,10 @@ type ImportMarkdownZipOptions = {
*/
async function exportDoc(doc: Store) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.workspace.createDoc({ id }),
get: (id: string) => doc.workspace.getDoc(id),
delete: (id: string) => doc.workspace.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(doc.workspace.id),
titleMiddleware(doc.workspace.meta.docMetas),
],
});
const job = doc.getTransformer([
docLinkBaseURLMiddleware(doc.workspace.id),
titleMiddleware(doc.workspace.meta.docMetas),
]);
const snapshot = job.docToSnapshot(doc);
const adapter = new MarkdownAdapter(job, provider);
@@ -107,19 +100,10 @@ async function importMarkdownToBlock({
blockId,
}: ImportMarkdownToBlockOptions) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.workspace.createDoc({ id }),
get: (id: string) => doc.workspace.getDoc(id),
delete: (id: string) => doc.workspace.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware(doc.workspace.id),
],
});
const job = doc.getTransformer([
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware(doc.workspace.id),
]);
const adapter = new MarkdownAdapter(job, provider);
const snapshot = await adapter.toSliceSnapshot({
file: markdown,
@@ -143,18 +127,20 @@ async function importMarkdownToBlock({
* Imports Markdown content into a new doc within a collection.
* @param options Object containing import options
* @param options.collection The target doc collection
* @param options.schema The schema of the target doc collection
* @param options.markdown The Markdown content to import
* @param options.fileName Optional filename for the imported doc
* @returns A Promise that resolves to the ID of the newly created doc, or undefined if import fails
*/
async function importMarkdownToDoc({
collection,
schema,
markdown,
fileName,
}: ImportMarkdownToDocOptions) {
const provider = getProvider();
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
@@ -182,11 +168,13 @@ async function importMarkdownToDoc({
* Imports a zip file containing Markdown files and assets into a collection.
* @param options Object containing import options
* @param options.collection The target doc collection
* @param options.schema The schema of the target doc collection
* @param options.imported The zip file as a Blob
* @returns A Promise that resolves to an array of IDs of the newly created docs
*/
async function importMarkdownZip({
collection,
schema,
imported,
}: ImportMarkdownZipOptions) {
const provider = getProvider();
@@ -219,7 +207,7 @@ async function importMarkdownZip({
markdownBlobs.map(async ([fileName, blob]) => {
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),

View File

@@ -3,12 +3,18 @@ import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { Container } from '@blocksuite/global/di';
import { sha } from '@blocksuite/global/utils';
import { extMimeMap, Transformer, type Workspace } from '@blocksuite/store';
import {
extMimeMap,
type Schema,
Transformer,
type Workspace,
} from '@blocksuite/store';
import { Unzip } from './utils.js';
type ImportNotionZipOptions = {
collection: Workspace;
schema: Schema;
imported: Blob;
};
@@ -26,6 +32,7 @@ function getProvider() {
*
* @param options - The options for importing.
* @param options.collection - The BlockSuite document collection.
* @param options.schema - The schema of the BlockSuite document collection.
* @param options.imported - The imported zip file as a Blob.
*
* @returns A promise that resolves to an object containing:
@@ -36,6 +43,7 @@ function getProvider() {
*/
async function importNotionZip({
collection,
schema,
imported,
}: ImportNotionZipOptions) {
const provider = getProvider();
@@ -117,7 +125,7 @@ async function importNotionZip({
}
const pagePromises = Array.from(pagePaths).map(async path => {
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),

View File

@@ -3,15 +3,19 @@ import {
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import { sha } from '@blocksuite/global/utils';
import type { DocSnapshot, Store, Workspace } from '@blocksuite/store';
import type { DocSnapshot, Schema, Store, Workspace } from '@blocksuite/store';
import { extMimeMap, getAssetName, Transformer } from '@blocksuite/store';
import { download, Unzip, Zip } from '../transformers/utils.js';
async function exportDocs(collection: Workspace, docs: Store[]) {
async function exportDocs(
collection: Workspace,
schema: Schema,
docs: Store[]
) {
const zip = new Zip();
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
@@ -70,7 +74,11 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
return download(downloadBlob, `${collection.id}.bs.zip`);
}
async function importDocs(collection: Workspace, imported: Blob) {
async function importDocs(
collection: Workspace,
schema: Schema,
imported: Blob
) {
const unzip = new Unzip();
await unzip.load(imported);
@@ -98,7 +106,7 @@ async function importDocs(collection: Workspace, imported: Blob) {
}
const job = new Transformer({
schema: collection.schema,
schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),

View File

@@ -134,13 +134,12 @@ export class EdgelessChangeBrushButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="color"
.label=${'Color'}
.label="${'Color'}"
.pick=${this.pickColor}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -159,7 +158,6 @@ export class EdgelessChangeBrushButton extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${selectedColor}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setBrushColor}
>
</edgeless-color-panel>

View File

@@ -373,13 +373,12 @@ export class EdgelessChangeConnectorButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="stroke-color"
.label=${'Stroke style'}
.label="${'Stroke style'}"
.pick=${this.pickColor}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${true}
>
<div

View File

@@ -13,7 +13,6 @@ import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar';
import {
type ColorScheme,
DEFAULT_NOTE_HEIGHT,
DefaultTheme,
type FrameBlockModel,
NoteBlockModel,
NoteDisplayMode,
@@ -201,13 +200,12 @@ export class EdgelessChangeFrameButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="background"
.label=${'Background'}
.label="${'Background'}"
.pick=${this.pickColor}
.color=${background}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -229,7 +227,6 @@ export class EdgelessChangeFrameButton extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${background}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setFrameBackground}
>
</edgeless-color-panel>

View File

@@ -338,7 +338,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -362,7 +361,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
aria-label="Fill colors"
.value=${selectedFillColor}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setShapeFillColor}
>
</edgeless-color-panel>
@@ -390,7 +388,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${true}
>
<div
@@ -453,8 +450,8 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
() => html`
<editor-icon-button
aria-label="Add text"
.tooltip=${'Add text'}
.iconSize=${'20px'}
.tooltip="${'Add text'}"
.iconSize="${'20px'}"
@click=${this._addText}
>
${AddTextIcon()}
@@ -465,7 +462,7 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
'menu',
() => html`
<edgeless-change-text-menu
.elementType=${'shape'}
.elementType="${'shape'}"
.elements=${elements}
.edgeless=${this.edgeless}
></edgeless-change-text-menu>

View File

@@ -344,6 +344,10 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
matchFontFaces.length === 1 &&
matchFontFaces[0].style === selectedFontStyle &&
matchFontFaces[0].weight === selectedFontWeight;
const palettes =
this.elementType === 'shape'
? DefaultTheme.ShapeTextColorPalettes
: DefaultTheme.Palettes;
return join(
[
@@ -389,14 +393,14 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="text-color"
.label=${'Text color'}
.label="${'Text color'}"
.pick=${this.pickColor}
.isText=${true}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.palettes=${palettes}
>
</edgeless-color-picker-button>
`;
@@ -418,7 +422,7 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${selectedColor}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.palettes=${palettes}
@select=${this._setTextColor}
></edgeless-color-panel>
</editor-menu-button>

View File

@@ -233,6 +233,7 @@ export function createNewDocMenuGroup(
};
showImportModal({
collection: doc.workspace,
schema: doc.schema,
onSuccess,
onFail,
});

View File

@@ -8,7 +8,7 @@ import {
} from '@blocksuite/affine-components/icons';
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import type { Schema, Workspace } from '@blocksuite/store';
import { html, LitElement, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
@@ -31,6 +31,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
constructor(
private readonly collection: Workspace,
private readonly schema: Schema,
private readonly onSuccess?: OnSuccessHandler,
private readonly onFail?: OnFailHandler,
private readonly abortController = new AbortController()
@@ -63,6 +64,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
}
const pageId = await HtmlTransformer.importHTMLToDoc({
collection: this.collection,
schema: this.schema,
html: text,
fileName,
});
@@ -93,6 +95,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
}
const pageId = await MarkdownTransformer.importMarkdownToDoc({
collection: this.collection,
schema: this.schema,
markdown: text,
fileName,
});
@@ -117,6 +120,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
const { entryId, pageIds, isWorkspaceFile, hasMarkdown } =
await NotionHtmlTransformer.importNotionZip({
collection: this.collection,
schema: this.schema,
imported: file,
});
needLoading && this.abortController.abort();

View File

@@ -1,4 +1,4 @@
import type { Workspace } from '@blocksuite/store';
import type { Schema, Workspace } from '@blocksuite/store';
import {
ImportDoc,
@@ -7,12 +7,14 @@ import {
} from './import-doc.js';
export function showImportModal({
schema,
collection,
onSuccess,
onFail,
container = document.body,
abortController = new AbortController(),
}: {
schema: Schema;
collection: Workspace;
onSuccess?: OnSuccessHandler;
onFail?: OnFailHandler;
@@ -22,6 +24,7 @@ export function showImportModal({
}) {
const importDoc = new ImportDoc(
collection,
schema,
onSuccess,
onFail,
abortController

View File

@@ -20,10 +20,8 @@ import { choose } from 'lit/directives/choose.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import {
type PageRootBlockComponent,
RootBlockConfigExtension,
} from '../../index.js';
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
import { RootBlockConfigExtension } from '../../root-config.js';
import {
type AFFINE_LINKED_DOC_WIDGET,
getMenus,

View File

@@ -423,8 +423,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
override mounted() {
const disposable = this.std.view.viewUpdated.on(payload => {
if (payload.type !== 'block') return;
if (
payload.type === 'add' &&
payload.method === 'add' &&
matchModels(payload.view.model, [RootBlockModel])
) {
disposable.dispose();

View File

@@ -50,7 +50,11 @@ export {
} from './adapters/index.js';
export type { SurfaceContext } from './surface-block.js';
export { SurfaceBlockComponent } from './surface-block.js';
export { SurfaceBlockModel, SurfaceBlockSchema } from './surface-model.js';
export {
SurfaceBlockModel,
SurfaceBlockSchema,
SurfaceBlockSchemaExtension,
} from './surface-model.js';
export type { SurfaceBlockService } from './surface-service.js';
export {
EdgelessSurfaceBlockSpec,

View File

@@ -5,7 +5,7 @@ import type {
import type { SurfaceBlockProps } from '@blocksuite/block-std/gfx';
import { SurfaceBlockModel as BaseSurfaceModel } from '@blocksuite/block-std/gfx';
import { DisposableGroup } from '@blocksuite/global/utils';
import { defineBlockSchema } from '@blocksuite/store';
import { BlockSchemaExtension, defineBlockSchema } from '@blocksuite/store';
import * as Y from 'yjs';
import { elementsCtorMap } from './element-model/index.js';
@@ -36,6 +36,9 @@ export const SurfaceBlockSchema = defineBlockSchema({
toModel: () => new SurfaceBlockModel(),
});
export const SurfaceBlockSchemaExtension =
BlockSchemaExtension(SurfaceBlockSchema);
export type SurfaceMiddleware = (surface: SurfaceBlockModel) => () => void;
export class SurfaceBlockModel extends BaseSurfaceModel {

View File

@@ -1,5 +1,5 @@
import type { ColorScheme, Palette } from '@blocksuite/affine-model';
import { resolveColor } from '@blocksuite/affine-model';
import { DefaultTheme, resolveColor } from '@blocksuite/affine-model';
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { html, LitElement } from 'lit';
@@ -188,7 +188,7 @@ export class EdgelessColorPickerButton extends WithDisposable(LitElement) {
accessor menuButton!: EditorMenuButton;
@property({ attribute: false })
accessor palettes: Palette[] = [];
accessor palettes: Palette[] = DefaultTheme.Palettes;
@property({ attribute: false })
accessor pick!: (event: PickColorEvent) => void;

View File

@@ -36,20 +36,22 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
cursor: pointer;
}
.footnote-content-default {
display: inline-block;
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
color: ${unsafeCSSVarV2('button/pureWhiteText')};
width: 14px;
height: 14px;
line-height: 14px;
font-size: 10px;
font-weight: 400;
border-radius: 50%;
text-align: center;
text-overflow: ellipsis;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
transition: background 0.3s ease-in-out;
.footnote-node {
.footnote-content-default {
display: inline-block;
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
color: ${unsafeCSSVarV2('button/pureWhiteText')};
width: 14px;
height: 14px;
line-height: 14px;
font-size: 10px;
font-weight: 400;
border-radius: 50%;
text-align: center;
text-overflow: ellipsis;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
transition: background 0.3s ease-in-out;
}
}
.footnote-node.hover-effect {

View File

@@ -13,8 +13,11 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
.toggle-icon {
display: flex;
align-items: start;
margin-top: 0.45em;
justify-content: start;
position: absolute;
width: 16px;
height: 16px;
top: calc((1em - 16px) / 2 + 5px);
left: 0;
transform: translateX(-100%);
border-radius: 4px;
@@ -22,6 +25,7 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.toggle-icon:hover {
background: var(--affine-hover-color);
}

View File

@@ -3,7 +3,11 @@ import type {
GfxElementGeometry,
} from '@blocksuite/block-std/gfx';
import { GfxCompatible } from '@blocksuite/block-std/gfx';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import type { EmbedCardStyle } from '../../utils/index.js';
import { AttachmentBlockTransformer } from './attachment-transformer.js';
@@ -86,6 +90,10 @@ export const AttachmentBlockSchema = defineBlockSchema({
toModel: () => new AttachmentBlockModel(),
});
export const AttachmentBlockSchemaExtension = BlockSchemaExtension(
AttachmentBlockSchema
);
export class AttachmentBlockModel
extends GfxCompatible<AttachmentBlockProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -3,7 +3,11 @@ import type {
GfxElementGeometry,
} from '@blocksuite/block-std/gfx';
import { GfxCompatible } from '@blocksuite/block-std/gfx';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import type { EmbedCardStyle, LinkPreviewData } from '../../utils/index.js';
@@ -54,6 +58,9 @@ export const BookmarkBlockSchema = defineBlockSchema({
toModel: () => new BookmarkBlockModel(),
});
export const BookmarkBlockSchemaExtension =
BlockSchemaExtension(BookmarkBlockSchema);
export class BookmarkBlockModel
extends GfxCompatible<BookmarkBlockProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -1,4 +1,9 @@
import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
type Text,
} from '@blocksuite/store';
interface CodeBlockProps {
text: Text;
@@ -30,6 +35,8 @@ export const CodeBlockSchema = defineBlockSchema({
toModel: () => new CodeBlockModel(),
});
export const CodeBlockSchemaExtension = BlockSchemaExtension(CodeBlockSchema);
export class CodeBlockModel extends BlockModel<CodeBlockProps> {
override text!: Text;
}

View File

@@ -1,5 +1,9 @@
import type { Text } from '@blocksuite/store';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import type { Column, SerializedCells, ViewBasicDataType } from './types.js';
@@ -28,3 +32,6 @@ export const DatabaseBlockSchema = defineBlockSchema({
},
toModel: () => new DatabaseBlockModel(),
});
export const DatabaseBlockSchemaExtension =
BlockSchemaExtension(DatabaseBlockSchema);

View File

@@ -1,4 +1,8 @@
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export const DividerBlockSchema = defineBlockSchema({
flavour: 'affine:divider',
@@ -15,3 +19,6 @@ type Props = {
};
export class DividerBlockModel extends BlockModel<Props> {}
export const DividerBlockSchemaExtension =
BlockSchemaExtension(DividerBlockSchema);

View File

@@ -3,7 +3,11 @@ import type {
GfxElementGeometry,
} from '@blocksuite/block-std/gfx';
import { GfxCompatible } from '@blocksuite/block-std/gfx';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import { z } from 'zod';
import {
@@ -76,6 +80,10 @@ export const EdgelessTextBlockSchema = defineBlockSchema({
},
});
export const EdgelessTextBlockSchemaExtension = BlockSchemaExtension(
EdgelessTextBlockSchema
);
export class EdgelessTextBlockModel
extends GfxCompatible<EdgelessTextProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedFigmaBlockProps,
@@ -20,3 +22,7 @@ export const EmbedFigmaBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedFigmaModel(),
props: (): EmbedFigmaBlockProps => defaultEmbedFigmaProps,
});
export const EmbedFigmaBlockSchemaExtension = BlockSchemaExtension(
EmbedFigmaBlockSchema
);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedGithubBlockProps,
@@ -29,3 +31,7 @@ export const EmbedGithubBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedGithubModel(),
props: (): EmbedGithubBlockProps => defaultEmbedGithubProps,
});
export const EmbedGithubBlockSchemaExtension = BlockSchemaExtension(
EmbedGithubBlockSchema
);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedHtmlBlockProps,
@@ -18,3 +20,6 @@ export const EmbedHtmlBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedHtmlModel(),
props: (): EmbedHtmlBlockProps => defaultEmbedHtmlProps,
});
export const EmbedHtmlBlockSchemaExtension =
BlockSchemaExtension(EmbedHtmlBlockSchema);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedLinkedDocBlockProps,
@@ -20,3 +22,7 @@ export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedLinkedDocModel(),
props: (): EmbedLinkedDocBlockProps => defaultEmbedLinkedDocBlockProps,
});
export const EmbedLinkedDocBlockSchemaExtension = BlockSchemaExtension(
EmbedLinkedDocBlockSchema
);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedLoomBlockProps,
@@ -22,3 +24,6 @@ export const EmbedLoomBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedLoomModel(),
props: (): EmbedLoomBlockProps => defaultEmbedLoomProps,
});
export const EmbedLoomBlockSchemaExtension =
BlockSchemaExtension(EmbedLoomBlockSchema);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedSyncedDocBlockProps,
@@ -21,3 +23,7 @@ export const EmbedSyncedDocBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedSyncedDocModel(),
props: (): EmbedSyncedDocBlockProps => defaultEmbedSyncedDocBlockProps,
});
export const EmbedSyncedDocBlockSchemaExtension = BlockSchemaExtension(
EmbedSyncedDocBlockSchema
);

View File

@@ -1,3 +1,5 @@
import { BlockSchemaExtension } from '@blocksuite/store';
import { createEmbedBlockSchema } from '../../../utils/index.js';
import {
type EmbedYoutubeBlockProps,
@@ -25,3 +27,7 @@ export const EmbedYoutubeBlockSchema = createEmbedBlockSchema({
toModel: () => new EmbedYoutubeModel(),
props: (): EmbedYoutubeBlockProps => defaultEmbedYoutubeProps,
});
export const EmbedYoutubeBlockSchemaExtension = BlockSchemaExtension(
EmbedYoutubeBlockSchema
);

View File

@@ -15,7 +15,12 @@ import {
hasDescendantElementImpl,
} from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/utils';
import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
type Text,
} from '@blocksuite/store';
import { z } from 'zod';
import { type Color, ColorSchema, DefaultTheme } from '../../themes/index.js';
@@ -57,6 +62,8 @@ export const FrameBlockSchema = defineBlockSchema({
},
});
export const FrameBlockSchemaExtension = BlockSchemaExtension(FrameBlockSchema);
export class FrameBlockModel
extends GfxCompatible<FrameBlockProps>(BlockModel)
implements GfxElementGeometry, GfxGroupCompatibleInterface

View File

@@ -3,7 +3,11 @@ import type {
GfxElementGeometry,
} from '@blocksuite/block-std/gfx';
import { GfxCompatible } from '@blocksuite/block-std/gfx';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import { ImageBlockTransformer } from './image-transformer.js';
@@ -40,6 +44,8 @@ export const ImageBlockSchema = defineBlockSchema({
toModel: () => new ImageBlockModel(),
});
export const ImageBlockSchemaExtension = BlockSchemaExtension(ImageBlockSchema);
export class ImageBlockModel
extends GfxCompatible<ImageBlockProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -3,7 +3,11 @@ import {
GfxCompatible,
type GfxElementGeometry,
} from '@blocksuite/block-std/gfx';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export type LatexProps = {
latex: string;
@@ -34,6 +38,8 @@ export const LatexBlockSchema = defineBlockSchema({
},
});
export const LatexBlockSchemaExtension = BlockSchemaExtension(LatexBlockSchema);
export class LatexBlockModel
extends GfxCompatible<LatexProps>(BlockModel)
implements GfxElementGeometry {}

View File

@@ -1,5 +1,9 @@
import type { Text } from '@blocksuite/store';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
// `toggle` type has been deprecated, do not use it
export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
@@ -38,6 +42,8 @@ export const ListBlockSchema = defineBlockSchema({
toModel: () => new ListBlockModel(),
});
export const ListBlockSchemaExtension = BlockSchemaExtension(ListBlockSchema);
export class ListBlockModel extends BlockModel<ListProps> {
override text!: Text;
}

View File

@@ -4,7 +4,11 @@ import type {
} from '@blocksuite/block-std/gfx';
import { GfxCompatible } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/utils';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
import { z } from 'zod';
import {
@@ -21,6 +25,7 @@ import {
StrokeStyleSchema,
} from '../../consts/note';
import { type Color, ColorSchema, DefaultTheme } from '../../themes';
import { TableModelFlavour } from '../table';
export const NoteZodSchema = z
.object({
@@ -47,7 +52,6 @@ export const NoteZodSchema = z
},
},
});
import { TableModelFlavour } from '../table';
export const NoteBlockSchema = defineBlockSchema({
flavour: 'affine:note',
@@ -92,6 +96,7 @@ export const NoteBlockSchema = defineBlockSchema({
},
});
export const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
export type NoteProps = {
background: Color;
displayMode: NoteDisplayMode;

View File

@@ -1,4 +1,9 @@
import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
type Text,
} from '@blocksuite/store';
export type ParagraphType =
| 'text'
@@ -37,6 +42,9 @@ export const ParagraphBlockSchema = defineBlockSchema({
toModel: () => new ParagraphBlockModel(),
});
export const ParagraphBlockSchemaExtension =
BlockSchemaExtension(ParagraphBlockSchema);
export class ParagraphBlockModel extends BlockModel<ParagraphProps> {
override text!: Text;

View File

@@ -1,5 +1,9 @@
import type { Text } from '@blocksuite/store';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export type RootBlockProps = {
title: Text;
@@ -51,3 +55,5 @@ export const RootBlockSchema = defineBlockSchema({
},
toModel: () => new RootBlockModel(),
});
export const RootBlockSchemaExtension = BlockSchemaExtension(RootBlockSchema);

View File

@@ -1,4 +1,8 @@
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export type SurfaceRefProps = {
reference: string;
@@ -21,4 +25,8 @@ export const SurfaceRefBlockSchema = defineBlockSchema({
toModel: () => new SurfaceRefBlockModel(),
});
export const SurfaceRefBlockSchemaExtension = BlockSchemaExtension(
SurfaceRefBlockSchema
);
export class SurfaceRefBlockModel extends BlockModel<SurfaceRefProps> {}

View File

@@ -1,6 +1,10 @@
import type { DeltaInsert } from '@blocksuite/inline';
import type { Text } from '@blocksuite/store';
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export type TableCell = {
text: Text;
@@ -56,3 +60,5 @@ export const TableBlockSchema = defineBlockSchema({
},
toModel: () => new TableBlockModel(),
});
export const TableBlockSchemaExtension = BlockSchemaExtension(TableBlockSchema);

View File

@@ -23,7 +23,7 @@ export const LINE_WIDTHS = [
];
/**
* Use `DefaultTheme.StrokeColorMap` instead.
* Use `DefaultTheme.StrokeColorShortMap` instead.
*
* @deprecated
*/
@@ -44,7 +44,7 @@ export enum LineColor {
export const LineColorMap = createEnumMap(LineColor);
/**
* Use `DefaultTheme.StrokeColorPalettes` instead.
* Use `DefaultTheme.StrokeColorShortPalettes` instead.
*
* @deprecated
*/

View File

@@ -77,11 +77,11 @@ export abstract class MindmapStyleGetter {
export class StyleOne extends MindmapStyleGetter {
private readonly _colorOrders = [
DefaultTheme.StrokeColorMap.Purple,
DefaultTheme.StrokeColorMap.Magenta,
DefaultTheme.StrokeColorMap.Orange,
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
DefaultTheme.StrokeColorShortMap.Purple,
DefaultTheme.StrokeColorShortMap.Magenta,
DefaultTheme.StrokeColorShortMap.Orange,
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
'#7ae2d5',
];
@@ -188,9 +188,9 @@ export const styleOne = new StyleOne();
export class StyleTwo extends MindmapStyleGetter {
private readonly _colorOrders = [
DefaultTheme.StrokeColorMap.Blue,
DefaultTheme.StrokeColorShortMap.Blue,
'#7ae2d5',
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorShortMap.Yellow,
];
readonly root = {
@@ -207,7 +207,7 @@ export class StyleTwo extends MindmapStyleGetter {
color: DefaultTheme.pureBlack,
filled: true,
fillColor: DefaultTheme.StrokeColorMap.Yellow,
fillColor: DefaultTheme.StrokeColorShortMap.Yellow,
padding: [11, 22] as [number, number],
@@ -298,8 +298,8 @@ export const styleTwo = new StyleTwo();
export class StyleThree extends MindmapStyleGetter {
private readonly _strokeColor = [
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
'#5cc7ba',
];
@@ -317,7 +317,7 @@ export class StyleThree extends MindmapStyleGetter {
color: DefaultTheme.pureBlack,
filled: true,
fillColor: DefaultTheme.StrokeColorMap.Yellow,
fillColor: DefaultTheme.StrokeColorShortMap.Yellow,
padding: [10, 22] as [number, number],
@@ -407,12 +407,12 @@ export const styleThree = new StyleThree();
export class StyleFour extends MindmapStyleGetter {
private readonly _colors = [
DefaultTheme.StrokeColorMap.Purple,
DefaultTheme.StrokeColorMap.Magenta,
DefaultTheme.StrokeColorMap.Orange,
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
DefaultTheme.StrokeColorMap.Blue,
DefaultTheme.StrokeColorShortMap.Purple,
DefaultTheme.StrokeColorShortMap.Magenta,
DefaultTheme.StrokeColorShortMap.Orange,
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
DefaultTheme.StrokeColorShortMap.Blue,
];
readonly root = {

View File

@@ -72,15 +72,44 @@ const NoteBackgroundColorPalettes: Palette[] = [
...buildPalettes(NoteBackgroundColorMap),
] as const;
const StrokeColorMap = { ...Medium, Black, White } as const;
const StrokeColorShortMap = { ...Medium, Black, White } as const;
const StrokeColorPalettes: Palette[] = [
...buildPalettes(StrokeColorMap),
const StrokeColorShortPalettes: Palette[] = [
...buildPalettes(StrokeColorShortMap),
] as const;
const FillColorMap = { ...Medium, Black, White } as const;
const FillColorShortMap = { ...Medium, Black, White } as const;
const FillColorPalettes: Palette[] = [...buildPalettes(FillColorMap)] as const;
const FillColorShortPalettes: Palette[] = [
...buildPalettes(FillColorShortMap),
] as const;
const ShapeTextColorShortMap = {
...Medium,
Black: pureBlack,
White: pureWhite,
} as const;
const ShapeTextColorShortPalettes: Palette[] = [
...buildPalettes({ ...ShapeTextColorShortMap }),
] as const;
const ShapeTextColorPalettes: Palette[] = [
// Light
...buildPalettes(Light, 'Light'),
{ key: 'Transparent', value: Transparent },
// Medium
...buildPalettes(Medium, 'Medium'),
{ key: 'White', value: pureWhite },
// Heavy
...buildPalettes(Heavy, 'Heavy'),
{ key: 'Black', value: pureBlack },
] as const;
export const DefaultTheme: Theme = {
pureBlack,
@@ -89,18 +118,19 @@ export const DefaultTheme: Theme = {
white: White,
transparent: Transparent,
textColor: Medium.Blue,
// Custom button should be selected by default,
// add transparent `ff` to distinguish `#000000`.
shapeTextColor: '#000000ff',
shapeTextColor: pureBlack,
shapeStrokeColor: Medium.Yellow,
shapeFillColor: Medium.Yellow,
connectorColor: Medium.Grey,
noteBackgrounColor: NoteBackgroundColorMap.White,
Palettes,
StrokeColorMap,
StrokeColorPalettes,
FillColorMap,
FillColorPalettes,
ShapeTextColorPalettes,
NoteBackgroundColorMap,
NoteBackgroundColorPalettes,
StrokeColorShortMap,
StrokeColorShortPalettes,
FillColorShortMap,
FillColorShortPalettes,
ShapeTextColorShortMap,
ShapeTextColorShortPalettes,
} as const;

View File

@@ -21,16 +21,20 @@ export const ThemeSchema = z.object({
shapeFillColor: ColorSchema,
connectorColor: ColorSchema,
noteBackgrounColor: ColorSchema,
// Universal color palette
// Universal color palettes
Palettes: z.array(PaletteSchema),
StrokeColorMap: z.record(z.string(), ColorSchema),
// Usually used in global toolbar and editor preview
StrokeColorPalettes: z.array(PaletteSchema),
FillColorMap: z.record(z.string(), ColorSchema),
// Usually used in global toolbar and editor preview
FillColorPalettes: z.array(PaletteSchema),
ShapeTextColorPalettes: z.array(PaletteSchema),
NoteBackgroundColorMap: z.record(z.string(), ColorSchema),
NoteBackgroundColorPalettes: z.array(PaletteSchema),
// Usually used in global toolbar and editor preview
StrokeColorShortMap: z.record(z.string(), ColorSchema),
StrokeColorShortPalettes: z.array(PaletteSchema),
FillColorShortMap: z.record(z.string(), ColorSchema),
FillColorShortPalettes: z.array(PaletteSchema),
ShapeTextColorShortMap: z.record(z.string(), ColorSchema),
ShapeTextColorShortPalettes: z.array(PaletteSchema),
});
export type Theme = z.infer<typeof ThemeSchema>;

View File

@@ -37,7 +37,7 @@ export class DNDAPIExtension extends Extension {
const { docId, flavour = 'affine:embed-linked-doc', blockId } = options;
const slice = Slice.fromModels(this.std.store, []);
const job = this.std.getTransformer();
const job = this.std.store.getTransformer();
const snapshot = job.sliceToSnapshot(slice);
if (!snapshot) {
console.error('Failed to convert slice to snapshot');

View File

@@ -1,8 +1,13 @@
import type { EditorHost } from '@blocksuite/block-std';
import { type Viewport } from '@blocksuite/block-std/gfx';
import { Pane } from 'tweakpane';
import { getSentenceRects, segmentSentences } from './text-utils.js';
import type { ParagraphLayout, ViewportLayout } from './types.js';
import type {
ParagraphLayout,
RenderingState,
ViewportLayout,
} from './types.js';
import type { ViewportTurboRendererExtension } from './viewport-renderer.js';
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
@@ -19,7 +24,7 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
}
export function getViewportLayout(
host: HTMLElement,
host: EditorHost,
viewport: Viewport
): ViewportLayout {
const paragraphBlocks = host.querySelectorAll(
@@ -98,15 +103,15 @@ export function initTweakpane(
paneElement.style.right = '10px';
paneElement.style.width = '250px';
debugPane.title = 'Viewport Turbo Renderer';
debugPane
.addBinding({ paused: false }, 'paused', {
label: 'Paused',
})
.on('change', ({ value }) => {
renderer.state = value ? 'paused' : 'monitoring';
});
debugPane.addButton({ title: 'Invalidate' }).on('click', () => {
renderer.invalidate();
});
}
export function debugLog(message: string, state: RenderingState) {
console.log(
`%c[ViewportTurboRenderer]%c ${message} | state=${state}`,
'color: #4285f4; font-weight: bold;',
'color: inherit;'
);
}

View File

@@ -8,6 +8,7 @@ type WorkerMessagePaint = {
height: number;
dpr: number;
zoom: number;
version: number;
};
};
@@ -63,7 +64,7 @@ class LayoutPainter {
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
paint(layout: ViewportLayout) {
paint(layout: ViewportLayout, version: number) {
const { canvas, ctx } = this;
if (!canvas || !ctx) return;
if (layout.rect.w === 0 || layout.rect.h === 0) {
@@ -103,7 +104,10 @@ class LayoutPainter {
});
const bitmap = canvas.transferToImageBitmap();
self.postMessage({ type: 'bitmapPainted', bitmap }, { transfer: [bitmap] });
self.postMessage(
{ type: 'bitmapPainted', bitmap, version },
{ transfer: [bitmap] }
);
}
}
@@ -127,9 +131,9 @@ self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
switch (type) {
case 'paintLayout': {
const { layout, width, height, dpr, zoom } = data;
const { layout, width, height, dpr, zoom, version } = data;
painter.setSize(width, height, dpr, zoom);
painter.paint(layout);
painter.paint(layout, version);
break;
}
}

View File

@@ -32,3 +32,12 @@ export interface TextRect {
rect: Rect;
text: string;
}
/**
* Represents the rendering state of the ViewportTurboRenderer
* - inactive: Renderer is not active
* - pending: Bitmap is invalid or not yet available, falling back to DOM rendering
* - rendering: Currently rendering to a bitmap (async operation in progress)
* - ready: Bitmap is valid and rendered, DOM elements can be safely removed
*/
export type RenderingState = 'inactive' | 'pending' | 'rendering' | 'ready';

View File

@@ -4,17 +4,20 @@ import {
LifeCycleWatcherIdentifier,
StdIdentifier,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di';
import {
GfxControllerIdentifier,
type GfxViewportElement,
} from '@blocksuite/block-std/gfx';
import type { Container, ServiceIdentifier } from '@blocksuite/global/di';
import { debounce, DisposableGroup } from '@blocksuite/global/utils';
import { type Pane } from 'tweakpane';
import {
debugLog,
getViewportLayout,
initTweakpane,
syncCanvasSize,
} from './dom-utils.js';
import { type ViewportLayout } from './types.js';
import type { RenderingState, ViewportLayout } from './types.js';
export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
'ViewportTurboRenderer'
@@ -25,12 +28,14 @@ interface Tile {
zoom: number;
}
// With high enough zoom, fallback to DOM rendering
const zoomThreshold = 1;
const zoomThreshold = 1; // With high enough zoom, fallback to DOM rendering
const debounceTime = 1000; // During this period, fallback to DOM
const debug = false; // Toggle for debug logs
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
state: 'monitoring' | 'paused' = 'paused';
state: RenderingState = 'inactive';
disposables = new DisposableGroup();
private layoutVersion = 0;
static override setup(di: Container) {
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
@@ -38,15 +43,16 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
private readonly worker: Worker;
private layoutCache: ViewportLayout | null = null;
private layoutCacheData: ViewportLayout | null = null;
private tile: Tile | null = null;
private debugPane: Pane | null = null;
private viewportElement: GfxViewportElement | null = null;
constructor(std: BlockStdScope) {
super(std);
this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), {
type: 'module',
});
this.debugLog('Initialized ViewportTurboRenderer');
}
override mounted() {
@@ -56,9 +62,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
initTweakpane(this, mountPoint as HTMLElement);
}
this.viewport.elementReady.once(() => {
this.viewport.elementReady.once(element => {
this.viewportElement = element;
syncCanvasSize(this.canvas, this.std.host);
this.state = 'monitoring';
this.setState('pending');
this.disposables.add(
this.viewport.sizeUpdated.on(() => this.handleResize())
);
this.disposables.add(
this.viewport.viewportUpdated.on(() => {
this.refresh().catch(console.error);
@@ -66,70 +77,91 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
);
});
const debouncedRefresh = debounce(
() => {
this.refresh().catch(console.error);
},
1000, // During this period, fallback to DOM
{ leading: false, trailing: true }
);
this.disposables.add(
this.std.store.slots.blockUpdated.on(() => {
this.invalidate();
debouncedRefresh();
this.debouncedRefresh();
})
);
}
override unmounted() {
this.debugLog('Unmounting renderer');
this.clearTile();
if (this.debugPane) {
this.debugPane.dispose();
this.debugPane = null;
}
this.clearOptimizedBlocks();
this.worker.terminate();
this.canvas.remove();
this.disposables.dispose();
this.setState('inactive');
}
get viewport() {
return this.std.get(GfxControllerIdentifier).viewport;
}
get layoutCache() {
if (this.layoutCacheData) return this.layoutCacheData;
const layout = getViewportLayout(this.std.host, this.viewport);
this.debugLog('Layout cache updated');
return (this.layoutCacheData = layout);
}
async refresh() {
if (this.state === 'paused') return;
if (this.state === 'inactive') return;
this.clearCanvas();
// -> pending
if (this.viewport.zoom > zoomThreshold) {
return;
} else if (this.canUseBitmapCache()) {
this.drawCachedBitmap(this.layoutCache!);
} else {
if (!this.layoutCache) {
this.updateLayoutCache();
}
const layout = this.layoutCache!;
await this.paintLayout(layout);
this.drawCachedBitmap(layout);
this.debugLog('Zoom above threshold, falling back to DOM rendering');
this.setState('pending');
this.toggleOptimization(false);
this.clearOptimizedBlocks();
}
// -> ready
else if (this.canUseBitmapCache()) {
this.debugLog('Using cached bitmap');
this.setState('ready');
this.drawCachedBitmap(this.layoutCache);
this.updateOptimizedBlocks();
}
// -> rendering
else {
this.setState('rendering');
this.toggleOptimization(false);
await this.paintLayout(this.layoutCache);
this.drawCachedBitmap(this.layoutCache);
this.updateOptimizedBlocks();
}
}
debouncedRefresh = debounce(
() => {
this.refresh().catch(console.error);
},
debounceTime,
{ leading: false, trailing: true }
);
invalidate() {
this.layoutCache = null;
this.layoutVersion++;
this.layoutCacheData = null;
this.clearTile();
this.clearCanvas(); // Should clear immediately after content updates
this.clearCanvas();
this.clearOptimizedBlocks();
this.setState('pending');
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
}
private updateLayoutCache() {
const layout = getViewportLayout(this.std.host, this.viewport);
this.layoutCache = layout;
private debugLog(message: string) {
if (!debug) return;
debugLog(message, this.state);
}
private clearTile() {
if (this.tile) {
this.tile.bitmap.close();
this.tile = null;
}
if (!this.tile) return;
this.tile.bitmap.close();
this.tile = null;
this.debugLog('Tile cleared');
}
private async paintLayout(layout: ViewportLayout): Promise<void> {
@@ -137,6 +169,9 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
if (!this.worker) return;
const dpr = window.devicePixelRatio;
const currentVersion = this.layoutVersion;
this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
this.worker.postMessage({
type: 'paintLayout',
data: {
@@ -145,25 +180,37 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
height: layout.rect.h,
dpr,
zoom: this.viewport.zoom,
version: currentVersion,
},
});
this.worker.onmessage = (e: MessageEvent) => {
if (e.data.type === 'bitmapPainted') {
this.handlePaintedBitmap(e.data.bitmap, resolve);
if (e.data.version === this.layoutVersion) {
this.debugLog(
`Bitmap painted successfully (version=${e.data.version})`
);
this.handlePaintedBitmap(e.data.bitmap, resolve);
} else {
this.debugLog(
`Received outdated bitmap (got=${e.data.version}, current=${this.layoutVersion})`
);
e.data.bitmap.close();
this.setState('pending');
resolve();
}
}
};
});
}
private handlePaintedBitmap(bitmap: ImageBitmap, resolve: () => void) {
if (this.tile) {
this.tile.bitmap.close();
}
this.clearTile();
this.tile = {
bitmap,
zoom: this.viewport.zoom,
};
this.setState('ready');
resolve();
}
@@ -177,10 +224,17 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const ctx = this.canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.debugLog('Canvas cleared');
}
private drawCachedBitmap(layout: ViewportLayout) {
const bitmap = this.tile!.bitmap;
if (!this.tile) {
this.debugLog('No cached bitmap available, requesting refresh');
this.debouncedRefresh();
return;
}
const bitmap = this.tile.bitmap;
const ctx = this.canvas.getContext('2d');
if (!ctx) return;
@@ -197,5 +251,61 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
);
this.debugLog('Bitmap drawn to canvas');
}
setState(newState: RenderingState) {
if (this.state === newState) return;
this.debugLog(`State change: ${this.state} -> ${newState}`);
this.state = newState;
}
canOptimize(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
}
private updateOptimizedBlocks() {
requestAnimationFrame(() => {
if (!this.viewportElement || !this.layoutCache) return;
if (!this.canOptimize()) return;
if (this.state !== 'ready') {
this.debugLog('Unexpected state updating optimized blocks');
console.warn('Unexpected state', this.tile, this.layoutCache);
return;
}
this.toggleOptimization(true);
const blockElements = this.viewportElement.getModelsInViewport();
const blockIds = Array.from(blockElements).map(model => model.id);
this.viewportElement.updateOptimizedBlocks(blockIds, true);
this.debugLog(`Optimized ${blockIds.length} blocks`);
});
}
private clearOptimizedBlocks() {
if (!this.viewportElement) return;
this.viewportElement.clearOptimizedBlocks();
this.debugLog('Cleared optimized blocks');
}
private toggleOptimization(value: boolean) {
if (
this.viewportElement &&
this.viewportElement.enableOptimization !== value
) {
this.viewportElement.enableOptimization = value;
this.debugLog(`${value ? 'Enabled' : 'Disabled'} optimization`);
}
}
private handleResize() {
this.debugLog('Container resized, syncing canvas size');
syncCanvasSize(this.canvas, this.std.host);
this.invalidate();
this.debouncedRefresh();
}
}

View File

@@ -145,6 +145,8 @@ export class PreviewHelper {
}
this.std.view.viewUpdated.on(payload => {
if (payload.type !== 'block') return;
if (payload.view.model.flavour === 'affine:page') {
const gfx = this.std.get(GfxControllerIdentifier);

View File

@@ -1354,7 +1354,7 @@ export class DragEventWatcher {
middlewares.push(gfxBlocksFilter(selectedIds, std));
}
return std.getTransformer(middlewares);
return std.store.getTransformer(middlewares);
}
private _isDropOnCurrentEditor(std?: BlockStdScope) {
@@ -1625,10 +1625,13 @@ export class DragEventWatcher {
disposables.add(
std.view.viewUpdated.on(payload => {
if (payload.type === 'add') {
if (payload.type !== 'block') {
return;
}
if (payload.method === 'add') {
this._makeDropTarget(payload.view);
} else if (
payload.type === 'delete' &&
payload.method === 'delete' &&
this.dropTargetCleanUps.has(payload.id)
) {
this.dropTargetCleanUps.get(payload.id)!.forEach(clean => clean());

View File

@@ -250,6 +250,7 @@ export class EdgelessRemoteSelectionWidget extends WidgetComponent<RootBlockMode
${MultiCursorDuotoneIcon({
width: '24px',
height: '24px',
style: `fill: ${_remoteColorManager.get(id)}; stroke: ${_remoteColorManager.get(id)};`,
})}
<div
class="remote-username"

View File

@@ -4,8 +4,11 @@ import type { SliceSnapshot } from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { createJob } from '../utils/create-job.js';
import { getProvider } from '../utils/get-provider.js';
import { nanoidReplacement } from '../utils/nanoid-replacement.js';
getProvider();
describe('notion-text to snapshot', () => {
test('basic', () => {
const notionText =

View File

@@ -11,39 +11,37 @@ import {
type Cell,
type Column,
type DatabaseBlockModel,
DatabaseBlockSchema,
NoteBlockSchema,
ParagraphBlockSchema,
RootBlockSchema,
DatabaseBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
} from '@blocksuite/affine-model';
import { propertyModelPresets } from '@blocksuite/data-view/property-pure-presets';
import type { BlockModel, Store } from '@blocksuite/store';
import { Schema, Text } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import {
createAutoIncrementIdGenerator,
TestWorkspace,
} from '@blocksuite/store/test';
import { beforeEach, describe, expect, test } from 'vitest';
const AffineSchemas = [
RootBlockSchema,
NoteBlockSchema,
ParagraphBlockSchema,
DatabaseBlockSchema,
const extensions = [
RootBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
DatabaseBlockSchemaExtension,
];
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register(AffineSchemas);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
function createTestDoc(docId = 'doc0') {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: docId });
const doc = collection.createDoc({ id: docId, extensions });
doc.load();
return doc;
}

View File

@@ -1,5 +1,5 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import {
Schema,
Transformer,
@@ -26,8 +26,8 @@ export function createJob(middlewares?: TransformerMiddleware[]) {
const testMiddlewares = middlewares ?? [];
testMiddlewares.push(defaultImageProxyMiddleware);
const schema = new Schema().register(AffineSchemas);
const docCollection = new TestWorkspace({ schema });
docCollection.storeExtensions = [FeatureFlagService];
const docCollection = new TestWorkspace();
docCollection.storeExtensions = SpecProvider._.getSpec('store').value;
docCollection.meta.initialize();
return new Transformer({
schema,

View File

@@ -2,15 +2,12 @@ import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
import { CodeBlockSpec } from '@blocksuite/affine-block-code';
import { DataViewBlockSpec } from '@blocksuite/affine-block-data-view';
import {
DatabaseBlockSpec,
DatabaseSelectionExtension,
} from '@blocksuite/affine-block-database';
import { DatabaseBlockSpec } from '@blocksuite/affine-block-database';
import { DividerBlockSpec } from '@blocksuite/affine-block-divider';
import { EdgelessTextBlockSpec } from '@blocksuite/affine-block-edgeless-text';
import { EmbedExtensions } from '@blocksuite/affine-block-embed';
import { FrameBlockSpec } from '@blocksuite/affine-block-frame';
import { ImageBlockSpec, ImageStoreSpec } from '@blocksuite/affine-block-image';
import { ImageBlockSpec } from '@blocksuite/affine-block-image';
import { LatexBlockSpec } from '@blocksuite/affine-block-latex';
import { ListBlockSpec } from '@blocksuite/affine-block-list';
import {
@@ -26,43 +23,19 @@ import {
EdgelessSurfaceRefBlockSpec,
PageSurfaceRefBlockSpec,
} from '@blocksuite/affine-block-surface-ref';
import {
TableBlockSpec,
TableSelectionExtension,
} from '@blocksuite/affine-block-table';
import { TableBlockSpec } from '@blocksuite/affine-block-table';
import {
RefNodeSlotsExtension,
RichTextExtensions,
} from '@blocksuite/affine-components/rich-text';
import {
HighlightSelectionExtension,
ImageSelectionExtension,
} from '@blocksuite/affine-shared/selection';
import {
DefaultOpenDocExtension,
DocDisplayMetaService,
EditPropsStore,
FeatureFlagService,
FileSizeLimitService,
FontLoaderService,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services';
import {
BlockSelectionExtension,
CursorSelectionExtension,
SurfaceSelectionExtension,
TextSelectionExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import {
AdapterFactoryExtensions,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
PlainTextAdapterExtension,
} from '../adapters/extension.js';
export const CommonBlockSpecs: ExtensionType[] = [
DocDisplayMetaService,
RefNodeSlotsExtension,
@@ -82,7 +55,6 @@ export const CommonBlockSpecs: ExtensionType[] = [
ParagraphBlockSpec,
DefaultOpenDocExtension,
FontLoaderService,
AdapterFactoryExtensions,
].flat();
export const PageFirstPartyBlockSpecs: ExtensionType[] = [
@@ -101,24 +73,3 @@ export const EdgelessFirstPartyBlockSpecs: ExtensionType[] = [
FrameBlockSpec,
EdgelessTextBlockSpec,
].flat();
export const StoreExtensions: ExtensionType[] = [
BlockSelectionExtension,
TextSelectionExtension,
SurfaceSelectionExtension,
CursorSelectionExtension,
HighlightSelectionExtension,
ImageSelectionExtension,
DatabaseSelectionExtension,
TableSelectionExtension,
FeatureFlagService,
LinkPreviewerService,
FileSizeLimitService,
ImageStoreSpec,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
PlainTextAdapterExtension,
].flat();

View File

@@ -1,3 +1,4 @@
export * from './common.js';
export * from './editor-specs.js';
export * from './preview-specs.js';
export * from './store.js';

View File

@@ -1,6 +1,5 @@
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { StoreExtensions } from './common.js';
import {
EdgelessEditorBlockSpecs,
PageEditorBlockSpecs,
@@ -9,6 +8,7 @@ import {
PreviewEdgelessEditorBlockSpecs,
PreviewPageEditorBlockSpecs,
} from './preview-specs.js';
import { StoreExtensions } from './store.js';
export function registerSpecs() {
SpecProvider._.addSpec('store', StoreExtensions);

View File

@@ -0,0 +1,101 @@
import { DataViewBlockSchemaExtension } from '@blocksuite/affine-block-data-view';
import { DatabaseSelectionExtension } from '@blocksuite/affine-block-database';
import { ImageStoreSpec } from '@blocksuite/affine-block-image';
import { SurfaceBlockSchemaExtension } from '@blocksuite/affine-block-surface';
import { TableSelectionExtension } from '@blocksuite/affine-block-table';
import {
AttachmentBlockSchemaExtension,
BookmarkBlockSchemaExtension,
CodeBlockSchemaExtension,
DatabaseBlockSchemaExtension,
DividerBlockSchemaExtension,
EdgelessTextBlockSchemaExtension,
EmbedFigmaBlockSchemaExtension,
EmbedGithubBlockSchemaExtension,
EmbedHtmlBlockSchemaExtension,
EmbedLinkedDocBlockSchemaExtension,
EmbedLoomBlockSchemaExtension,
EmbedSyncedDocBlockSchemaExtension,
EmbedYoutubeBlockSchemaExtension,
FrameBlockSchemaExtension,
ImageBlockSchemaExtension,
LatexBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
SurfaceRefBlockSchemaExtension,
TableBlockSchemaExtension,
} from '@blocksuite/affine-model';
import {
HighlightSelectionExtension,
ImageSelectionExtension,
} from '@blocksuite/affine-shared/selection';
import {
FeatureFlagService,
FileSizeLimitService,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services';
import {
BlockSelectionExtension,
CursorSelectionExtension,
SurfaceSelectionExtension,
TextSelectionExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import {
AdapterFactoryExtensions,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
PlainTextAdapterExtension,
} from '../adapters/extension.js';
export const StoreExtensions: ExtensionType[] = [
CodeBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
DividerBlockSchemaExtension,
ImageBlockSchemaExtension,
SurfaceBlockSchemaExtension,
BookmarkBlockSchemaExtension,
FrameBlockSchemaExtension,
DatabaseBlockSchemaExtension,
SurfaceRefBlockSchemaExtension,
DataViewBlockSchemaExtension,
AttachmentBlockSchemaExtension,
EmbedSyncedDocBlockSchemaExtension,
EmbedLinkedDocBlockSchemaExtension,
EmbedHtmlBlockSchemaExtension,
EmbedGithubBlockSchemaExtension,
EmbedFigmaBlockSchemaExtension,
EmbedLoomBlockSchemaExtension,
EmbedYoutubeBlockSchemaExtension,
EdgelessTextBlockSchemaExtension,
LatexBlockSchemaExtension,
TableBlockSchemaExtension,
BlockSelectionExtension,
TextSelectionExtension,
SurfaceSelectionExtension,
CursorSelectionExtension,
HighlightSelectionExtension,
ImageSelectionExtension,
DatabaseSelectionExtension,
TableSelectionExtension,
FeatureFlagService,
LinkPreviewerService,
FileSizeLimitService,
ImageStoreSpec,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
PlainTextAdapterExtension,
AdapterFactoryExtensions,
].flat();

View File

@@ -1,4 +1,3 @@
import { Schema } from '@blocksuite/store';
import {
createAutoIncrementIdGenerator,
TestWorkspace,
@@ -9,19 +8,23 @@ import { effects } from '../effects.js';
import { TestEditorContainer } from './test-editor.js';
import {
type HeadingBlockModel,
HeadingBlockSchema,
NoteBlockSchema,
RootBlockSchema,
HeadingBlockSchemaExtension,
NoteBlockSchemaExtension,
RootBlockSchemaExtension,
} from './test-schema.js';
import { testSpecs } from './test-spec.js';
effects();
const extensions = [
RootBlockSchemaExtension,
NoteBlockSchemaExtension,
HeadingBlockSchemaExtension,
];
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register([RootBlockSchema, NoteBlockSchema, HeadingBlockSchema]);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
function wait(time: number) {
@@ -33,7 +36,7 @@ describe('editor host', () => {
const collection = new TestWorkspace(createTestOptions());
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
const rootId = doc.addBlock('test:page');
const noteId = doc.addBlock('test:note', {}, rootId);

View File

@@ -1,4 +1,8 @@
import { BlockModel, defineBlockSchema } from '@blocksuite/store';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
} from '@blocksuite/store';
export const RootBlockSchema = defineBlockSchema({
flavour: 'test:page',
@@ -15,6 +19,8 @@ export const RootBlockSchema = defineBlockSchema({
},
});
export const RootBlockSchemaExtension = BlockSchemaExtension(RootBlockSchema);
export class RootBlockModel extends BlockModel<
ReturnType<(typeof RootBlockSchema)['model']['props']>
> {}
@@ -30,6 +36,8 @@ export const NoteBlockSchema = defineBlockSchema({
},
});
export const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
export class NoteBlockModel extends BlockModel<
ReturnType<(typeof NoteBlockSchema)['model']['props']>
> {}
@@ -47,6 +55,9 @@ export const HeadingBlockSchema = defineBlockSchema({
},
});
export const HeadingBlockSchemaExtension =
BlockSchemaExtension(HeadingBlockSchema);
export class HeadingBlockModel extends BlockModel<
ReturnType<(typeof HeadingBlockSchema)['model']['props']>
> {}

View File

@@ -285,7 +285,7 @@ export class Clipboard extends LifeCycleWatcher {
}
private _getJob() {
return this.std.getTransformer(this._jobMiddlewares);
return this.std.store.getTransformer(this._jobMiddlewares);
}
readFromClipboard(clipboardData: DataTransfer) {

View File

@@ -5,5 +5,4 @@ export * from './flavour.js';
export * from './keymap.js';
export * from './lifecycle-watcher.js';
export * from './service.js';
export * from './service-watcher.js';
export * from './widget-view-map.js';

View File

@@ -1,47 +0,0 @@
import type { Container } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
BlockServiceIdentifier,
LifeCycleWatcherIdentifier,
StdIdentifier,
} from '../identifier.js';
import type { BlockStdScope } from '../scope/index.js';
import { LifeCycleWatcher } from './lifecycle-watcher.js';
import type { BlockService } from './service.js';
const idMap = new Map<string, number>();
/**
* @deprecated
* BlockServiceWatcher is deprecated. You should reconsider where to put your feature.
*
* BlockServiceWatcher is a legacy extension that is used to watch the slots registered on block service.
* However, we recommend using the new extension system.
*/
export abstract class BlockServiceWatcher extends LifeCycleWatcher {
static flavour: string;
constructor(
std: BlockStdScope,
readonly blockService: BlockService
) {
super(std);
}
static override setup(di: Container) {
if (!this.flavour) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'Flavour is not defined in the BlockServiceWatcher'
);
}
const id = idMap.get(this.flavour) ?? 0;
idMap.set(this.flavour, id + 1);
di.addImpl(
LifeCycleWatcherIdentifier(`${this.flavour}-watcher-${id}`),
this,
[StdIdentifier, BlockServiceIdentifier(this.flavour)]
);
}
}

View File

@@ -1,6 +1,12 @@
import { assertType, type Constructor, Slot } from '@blocksuite/global/utils';
import {
assertType,
type Constructor,
DisposableGroup,
Slot,
} from '@blocksuite/global/utils';
import type { Boxed } from '@blocksuite/store';
import { BlockModel, nanoid } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import * as Y from 'yjs';
import {
@@ -98,6 +104,8 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
oldValues: Record<string, unknown>;
}>();
private readonly _isEmpty$ = signal(false);
get elementModels() {
const models: GfxPrimitiveElementModel[] = [];
this._elementModels.forEach(model => models.push(model.model));
@@ -113,7 +121,7 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
}
override isEmpty(): boolean {
return this._elementModels.size === 0 && this.children.length === 0;
return this._isEmpty$.value;
}
constructor() {
@@ -370,6 +378,7 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
if (isGfxGroupCompatibleModel(payload.model)) {
this._groupLikeModels.set(payload.id, payload.model);
}
break;
case 'delete':
if (isGfxGroupCompatibleModel(payload.model)) {
@@ -382,6 +391,7 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
group.removeChild(payload.model as GfxModel);
}
}
break;
}
});
@@ -445,6 +455,25 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
});
}
private _watchChildrenChange() {
const updateIsEmpty = () => {
this._isEmpty$.value =
this._elementModels.size === 0 && this.children.length === 0;
};
const disposables = new DisposableGroup();
disposables.add(this.elementAdded.on(updateIsEmpty));
disposables.add(this.elementRemoved.on(updateIsEmpty));
this.doc.slots.blockUpdated.on(payload => {
if (['add', 'delete'].includes(payload.type)) {
updateIsEmpty();
}
});
this.deleted.on(() => {
disposables.dispose();
});
}
protected _extendElement(
ctorMap: Record<
string,
@@ -460,6 +489,7 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
protected _init() {
this._initElementModels();
this._watchGroupRelationChange();
this._watchChildrenChange();
}
getConstructor(type: string) {

View File

@@ -57,27 +57,30 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
}
`;
optimizedBlocks = new Set<string>();
private readonly _hideOutsideBlock = () => {
if (this.getModelsInViewport && this.host) {
const host = this.host;
const modelsInViewport = this.getModelsInViewport();
if (!this.host) return;
modelsInViewport.forEach(model => {
const view = host.std.view.getBlock(model.id);
setDisplay(view, 'block');
const { host, optimizedBlocks, enableOptimization } = this;
const modelsInViewport = this.getModelsInViewport();
modelsInViewport.forEach(model => {
const view = host.std.view.getBlock(model.id);
const canOptimize = optimizedBlocks.has(model.id) && enableOptimization;
const display = canOptimize ? 'none' : 'block';
setDisplay(view, display);
if (this._lastVisibleModels?.has(model)) {
this._lastVisibleModels!.delete(model);
}
});
if (this._lastVisibleModels?.has(model)) {
this._lastVisibleModels!.delete(model);
}
});
this._lastVisibleModels?.forEach(model => {
const view = host.std.view.getBlock(model.id);
setDisplay(view, 'none');
});
this._lastVisibleModels?.forEach(model => {
const view = host.std.view.getBlock(model.id);
setDisplay(view, 'none');
});
this._lastVisibleModels = modelsInViewport;
}
this._lastVisibleModels = modelsInViewport;
};
private _lastVisibleModels?: Set<GfxBlockElementModel>;
@@ -154,7 +157,8 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
};
@property({ attribute: false })
accessor getModelsInViewport: undefined | (() => Set<GfxBlockElementModel>);
accessor getModelsInViewport: () => Set<GfxBlockElementModel> = () =>
new Set();
@property({ attribute: false })
accessor host: undefined | EditorHost;
@@ -167,4 +171,29 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor viewport!: Viewport;
@property({ attribute: false })
accessor enableOptimization: boolean = false;
updateOptimizedBlocks(blockIds: string[], optimized: boolean): void {
let changed = false;
blockIds.forEach(id => {
if (optimized && !this.optimizedBlocks.has(id)) {
this.optimizedBlocks.add(id);
changed = true;
} else if (!optimized && this.optimizedBlocks.has(id)) {
this.optimizedBlocks.delete(id);
changed = true;
}
});
if (changed) this._refreshViewport();
}
clearOptimizedBlocks(): void {
if (this.optimizedBlocks.size === 0) return;
this.optimizedBlocks.clear();
this._refreshViewport();
}
}

View File

@@ -4,8 +4,6 @@ import {
type ExtensionType,
type Store,
StoreSelectionExtension,
Transformer,
type TransformerMiddleware,
} from '@blocksuite/store';
import { Clipboard } from '../clipboard/index.js';
@@ -140,19 +138,6 @@ export class BlockStdScope {
return this.getOptional(BlockViewIdentifier(flavour));
}
getTransformer(middlewares: TransformerMiddleware[] = []) {
return new Transformer({
schema: this.workspace.schema,
blobCRUD: this.workspace.blobSync,
docCRUD: {
create: (id: string) => this.workspace.createDoc({ id }),
get: (id: string) => this.workspace.getDoc(id),
delete: (id: string) => this.workspace.removeDoc(id),
},
middlewares,
});
}
mount() {
this._lifeCycleWatchers.forEach(watcher => {
watcher.mounted();

View File

@@ -3,16 +3,20 @@ import { Slot } from '@blocksuite/global/utils';
import { LifeCycleWatcher } from '../extension/index.js';
import type { BlockComponent, WidgetComponent } from './element/index.js';
type ViewUpdatePayload =
type ViewUpdateMethod = 'delete' | 'add';
export type ViewUpdatePayload =
| {
id: string;
type: 'delete';
method: ViewUpdateMethod;
type: 'block';
view: BlockComponent;
}
| {
id: string;
type: 'add';
view: BlockComponent;
method: ViewUpdateMethod;
type: 'widget';
view: WidgetComponent;
};
export class ViewStore extends LifeCycleWatcher {
@@ -42,7 +46,8 @@ export class ViewStore extends LifeCycleWatcher {
this._blockMap.delete(node.model.id);
this.viewUpdated.emit({
id: node.model.id,
type: 'delete',
method: 'delete',
type: 'block',
view: node,
});
};
@@ -51,6 +56,12 @@ export class ViewStore extends LifeCycleWatcher {
const id = node.dataset.widgetId as string;
const widgetIndex = `${node.model.id}|${id}`;
this._widgetMap.delete(widgetIndex);
this.viewUpdated.emit({
id: node.model.id,
method: 'delete',
type: 'widget',
view: node,
});
};
getBlock = (id: string): BlockComponent | null => {
@@ -72,7 +83,8 @@ export class ViewStore extends LifeCycleWatcher {
this._blockMap.set(node.model.id, node);
this.viewUpdated.emit({
id: node.model.id,
type: 'add',
method: 'add',
type: 'block',
view: node,
});
};
@@ -81,6 +93,12 @@ export class ViewStore extends LifeCycleWatcher {
const id = node.dataset.widgetId as string;
const widgetIndex = `${node.model.id}|${id}`;
this._widgetMap.set(widgetIndex, node);
this.viewUpdated.emit({
id: node.model.id,
method: 'add',
type: 'widget',
view: node,
});
};
walkThrough = (

View File

@@ -2,6 +2,7 @@ import { computed, effect } from '@preact/signals-core';
import { describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
import { BlockSchemaExtension } from '../extension/schema.js';
import {
Block,
BlockModel,
@@ -9,7 +10,6 @@ import {
internalPrimitives,
} from '../model/block/index.js';
import type { YBlock } from '../model/block/types.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
@@ -27,6 +27,7 @@ const pageSchema = defineBlockSchema({
version: 1,
},
});
const pageSchemaExtension = BlockSchemaExtension(pageSchema);
const tableSchema = defineBlockSchema({
flavour: 'table',
@@ -39,6 +40,7 @@ const tableSchema = defineBlockSchema({
version: 1,
},
});
const tableSchemaExtension = BlockSchemaExtension(tableSchema);
const flatTableSchema = defineBlockSchema({
flavour: 'flat-table',
@@ -54,6 +56,8 @@ const flatTableSchema = defineBlockSchema({
isFlatData: true,
},
});
const flatTableSchemaExtension = BlockSchemaExtension(flatTableSchema);
class RootModel extends BlockModel<
ReturnType<(typeof pageSchema)['model']['props']>
> {}
@@ -66,9 +70,7 @@ class FlatTableModel extends BlockModel<
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register([pageSchema, tableSchema, flatTableSchema]);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
const defaultDocId = 'doc:home';
@@ -76,7 +78,14 @@ function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: docId });
const doc = collection.createDoc({
id: docId,
extensions: [
pageSchemaExtension,
tableSchemaExtension,
flatTableSchemaExtension,
],
});
doc.load();
return doc;
}

View File

@@ -4,29 +4,20 @@ import type { Slot } from '@blocksuite/global/utils';
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
import { applyUpdate, type Doc, encodeStateAsUpdate } from 'yjs';
import type { BlockModel, BlockSchemaType, DocMeta, Store } from '../index.js';
import { Schema } from '../index.js';
import type { BlockModel, DocMeta, Store } from '../index.js';
import { Text } from '../reactive/text.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import {
NoteBlockSchema,
ParagraphBlockSchema,
RootBlockSchema,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
} from './test-schema.js';
import { assertExists } from './test-utils-dom.js';
export const BlockSchemas = [
ParagraphBlockSchema,
RootBlockSchema,
NoteBlockSchema,
] as BlockSchemaType[];
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register(BlockSchemas);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
const defaultDocId = 'doc:home';
@@ -58,11 +49,20 @@ function createRoot(doc: Store) {
return doc.root;
}
const extensions = [
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
];
function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: docId });
const doc = collection.createDoc({
id: docId,
extensions,
});
doc.load();
return doc;
}
@@ -113,13 +113,6 @@ describe('basic', () => {
tags: [],
},
],
workspaceVersion: 2,
pageVersion: 2,
blockVersions: {
'affine:note': 1,
'affine:page': 2,
'affine:paragraph': 1,
},
},
spaces: {
[spaceId]: {
@@ -155,6 +148,7 @@ describe('basic', () => {
collection.meta.initialize();
const doc = collection.createDoc({
id: 'space:0',
extensions,
});
const readyCallback = vi.fn();
@@ -181,6 +175,7 @@ describe('basic', () => {
const collection2 = new TestWorkspace(options);
const doc = collection.createDoc({
id: 'space:0',
extensions,
});
doc.load(() => {
doc.addBlock('affine:page', {
@@ -209,7 +204,9 @@ describe('basic', () => {
// apply doc update
const update = encodeStateAsUpdate(doc.spaceDoc);
expect(collection2.docs.size).toBe(1);
const doc2 = collection2.getDoc('space:0');
const doc2 = collection2.getDoc('space:0', {
extensions,
});
assertExists(doc2);
applyUpdate(doc2.spaceDoc, update);
expect(serializCollection(collection2.doc)['spaces']).toEqual({

View File

@@ -2,31 +2,28 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
import type { BlockModel, Store } from '../model/index.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import {
DividerBlockSchema,
ListBlockSchema,
NoteBlockSchema,
ParagraphBlockSchema,
DividerBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
type RootBlockModel,
RootBlockSchema,
RootBlockSchemaExtension,
} from './test-schema.js';
const BlockSchemas = [
RootBlockSchema,
ParagraphBlockSchema,
ListBlockSchema,
NoteBlockSchema,
DividerBlockSchema,
const extensions = [
RootBlockSchemaExtension,
ParagraphBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
DividerBlockSchemaExtension,
];
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register(BlockSchemas);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
test('trigger props updated', () => {
@@ -34,7 +31,7 @@ test('trigger props updated', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
doc.addBlock('affine:page');
@@ -94,7 +91,7 @@ test('stash and pop', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
doc.addBlock('affine:page');
@@ -164,7 +161,7 @@ test('always get latest value in onChange', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
doc.addBlock('affine:page');
@@ -210,11 +207,12 @@ test('query', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc1 = collection.createDoc({ id: 'home' });
const doc1 = collection.createDoc({ id: 'home', extensions });
doc1.load();
const doc2 = collection.getDoc('home');
const doc2 = collection.getDoc('home', { extensions });
const doc3 = collection.getDoc('home', {
extensions,
query: {
mode: 'loose',
match: [
@@ -247,10 +245,10 @@ test('local readonly', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc1 = collection.createDoc({ id: 'home' });
const doc1 = collection.createDoc({ id: 'home', extensions });
doc1.load();
const doc2 = collection.getDoc('home', { readonly: true });
const doc3 = collection.getDoc('home', { readonly: false });
const doc2 = collection.getDoc('home', { readonly: true, extensions });
const doc3 = collection.getDoc('home', { readonly: false, extensions });
expect(doc1.readonly).toBeFalsy();
expect(doc2?.readonly).toBeTruthy();
@@ -276,7 +274,7 @@ describe('move blocks', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
const pageId = doc.addBlock('affine:page');
const page = doc.getBlock(pageId)!.model;

View File

@@ -1,25 +1,23 @@
import { literal } from 'lit/static-html.js';
import { describe, expect, it, vi } from 'vitest';
import { BlockSchemaExtension } from '../extension/schema.js';
import { defineBlockSchema } from '../model/block/zod.js';
// import some blocks
import { SchemaValidateError } from '../schema/error.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import {
DividerBlockSchema,
ListBlockSchema,
NoteBlockSchema,
ParagraphBlockSchema,
RootBlockSchema,
DividerBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
} from './test-schema.js';
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register(BlockSchemas);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
const TestCustomNoteBlockSchema = defineBlockSchema({
@@ -35,6 +33,10 @@ const TestCustomNoteBlockSchema = defineBlockSchema({
},
});
const TestCustomNoteBlockSchemaExtension = BlockSchemaExtension(
TestCustomNoteBlockSchema
);
const TestInvalidNoteBlockSchema = defineBlockSchema({
flavour: 'affine:note-invalid-block-video',
props: internal => ({
@@ -48,14 +50,18 @@ const TestInvalidNoteBlockSchema = defineBlockSchema({
},
});
const BlockSchemas = [
RootBlockSchema,
ParagraphBlockSchema,
ListBlockSchema,
NoteBlockSchema,
DividerBlockSchema,
TestCustomNoteBlockSchema,
TestInvalidNoteBlockSchema,
const TestInvalidNoteBlockSchemaExtension = BlockSchemaExtension(
TestInvalidNoteBlockSchema
);
const extensions = [
RootBlockSchemaExtension,
ParagraphBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
DividerBlockSchemaExtension,
TestCustomNoteBlockSchemaExtension,
TestInvalidNoteBlockSchemaExtension,
];
const defaultDocId = 'doc0';
@@ -63,7 +69,7 @@ function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: docId });
const doc = collection.createDoc({ id: docId, extensions });
doc.load();
return doc;
}

View File

@@ -1,3 +1,4 @@
import { BlockSchemaExtension } from '../extension/schema.js';
import { BlockModel, defineBlockSchema } from '../model/index.js';
export const RootBlockSchema = defineBlockSchema({
@@ -14,6 +15,8 @@ export const RootBlockSchema = defineBlockSchema({
},
});
export const RootBlockSchemaExtension = BlockSchemaExtension(RootBlockSchema);
export class RootBlockModel extends BlockModel<
ReturnType<(typeof RootBlockSchema)['model']['props']>
> {}
@@ -42,6 +45,8 @@ export const NoteBlockSchema = defineBlockSchema({
},
});
export const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
export const ParagraphBlockSchema = defineBlockSchema({
flavour: 'affine:paragraph',
props: internal => ({
@@ -60,6 +65,9 @@ export const ParagraphBlockSchema = defineBlockSchema({
},
});
export const ParagraphBlockSchemaExtension =
BlockSchemaExtension(ParagraphBlockSchema);
export const ListBlockSchema = defineBlockSchema({
flavour: 'affine:list',
props: internal => ({
@@ -80,6 +88,8 @@ export const ListBlockSchema = defineBlockSchema({
},
});
export const ListBlockSchemaExtension = BlockSchemaExtension(ListBlockSchema);
export const DividerBlockSchema = defineBlockSchema({
flavour: 'affine:divider',
metadata: {
@@ -88,3 +98,6 @@ export const DividerBlockSchema = defineBlockSchema({
children: [],
},
});
export const DividerBlockSchemaExtension =
BlockSchemaExtension(DividerBlockSchema);

View File

@@ -2,10 +2,10 @@ import { expect, test } from 'vitest';
import * as Y from 'yjs';
import { MemoryBlobCRUD } from '../adapter/index.js';
import { BlockSchemaExtension } from '../extension/schema.js';
import { BlockModel } from '../model/block/block-model.js';
import { defineBlockSchema } from '../model/block/zod.js';
import { Text } from '../reactive/index.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import { AssetsManager, BaseBlockTransformer } from '../transformer/index.js';
@@ -39,15 +39,16 @@ const docSchema = defineBlockSchema({
},
});
const docSchemaExtension = BlockSchemaExtension(docSchema);
class RootBlockModel extends BlockModel<
ReturnType<(typeof docSchema)['model']['props']>
> {}
const extensions = [docSchemaExtension];
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
schema.register([docSchema]);
return { id: 'test-collection', idGenerator, schema };
return { id: 'test-collection', idGenerator };
}
const transformer = new BaseBlockTransformer(new Map());
@@ -58,7 +59,7 @@ test('model to snapshot', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
doc.addBlock('page');
const rootModel = doc.root as RootBlockModel;
@@ -75,7 +76,7 @@ test('snapshot to model', async () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home' });
const doc = collection.createDoc({ id: 'home', extensions });
doc.load();
doc.addBlock('page');
const rootModel = doc.root as RootBlockModel;

View File

@@ -1,3 +1,4 @@
export * from './extension';
export * from './schema';
export * from './selection';
export * from './store-extension';

View File

@@ -0,0 +1,20 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { BlockSchemaType } from '../model/block/zod';
import type { ExtensionType } from './extension';
export const BlockSchemaIdentifier =
createIdentifier<BlockSchemaType>('BlockSchema');
export function BlockSchemaExtension(
blockSchema: BlockSchemaType
): ExtensionType {
return {
setup: di => {
di.addImpl(
BlockSchemaIdentifier(blockSchema.model.flavour),
() => blockSchema
);
},
};
}

View File

@@ -15,10 +15,14 @@ export function toDraftModel<Model extends BlockModel = BlockModel>(
origin: Model
): DraftModel<Model> {
const { id, version, flavour, role, keys, text, children } = origin;
const isFlatData = origin.schema.model.isFlatData;
const props = origin.keys.reduce((acc, key) => {
const target = isFlatData ? origin.props : origin;
const value = target[key as keyof typeof target];
return {
...acc,
[key]: origin[key as keyof Model],
[key]: value,
};
}, {} as ModelProps<Model>);

View File

@@ -1,7 +1,6 @@
import type { Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs';
import type { Schema } from '../schema/schema.js';
import type { AwarenessStore } from '../yjs/awareness.js';
import type { YBlock } from './block/types.js';
import type { Query } from './store/query.js';
@@ -18,7 +17,6 @@ export type YBlocks = Y.Map<YBlock>;
export interface Doc {
readonly id: string;
get meta(): DocMeta | undefined;
get schema(): Schema;
remove(): void;
load(initFn?: () => void): void;

View File

@@ -1,11 +1,16 @@
import { Container, type ServiceProvider } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { type Disposable, Slot } from '@blocksuite/global/utils';
import { signal } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import type { ExtensionType } from '../../extension/extension.js';
import { StoreSelectionExtension } from '../../extension/index.js';
import type { Schema } from '../../schema/index.js';
import {
BlockSchemaIdentifier,
StoreSelectionExtension,
} from '../../extension/index.js';
import { Schema } from '../../schema/index.js';
import type { TransformerMiddleware } from '../../transformer/middleware.js';
import { Transformer } from '../../transformer/transformer.js';
import {
Block,
type BlockModel,
@@ -20,7 +25,6 @@ import { type Query, runQuery } from './query.js';
import { syncBlockProps } from './utils.js';
export type StoreOptions = {
schema: Schema;
doc: Doc;
id?: string;
readonly?: boolean;
@@ -55,6 +59,10 @@ export class Store {
private readonly _readonly = signal(false);
private readonly _isEmpty = computed(() => {
return this.root?.isEmpty() ?? true;
});
private readonly _schema: Schema;
readonly slots: Doc['slots'] & {
@@ -215,7 +223,11 @@ export class Store {
}
get isEmpty() {
return this.root?.isEmpty() ?? true;
return this._isEmpty.peek();
}
get isEmpty$() {
return this._isEmpty;
}
get loaded() {
@@ -290,14 +302,7 @@ export class Store {
return this._doc.withoutTransact.bind(this._doc);
}
constructor({
schema,
doc,
readonly,
query,
provider,
extensions,
}: StoreOptions) {
constructor({ doc, readonly, query, provider, extensions }: StoreOptions) {
const container = new Container();
container.addImpl(StoreIdentifier, () => this);
@@ -323,8 +328,11 @@ export class Store {
yBlockUpdated: this._doc.slots.yBlockUpdated,
};
this._crud = new DocCRUD(this._yBlocks, doc.schema);
this._schema = schema;
this._schema = new Schema();
this._provider.getAll(BlockSchemaIdentifier).forEach(schema => {
this._schema.register([schema]);
});
this._crud = new DocCRUD(this._yBlocks, this._schema);
if (readonly !== undefined) {
this._readonly.value = readonly;
}
@@ -709,4 +717,17 @@ export class Store {
get getOptional() {
return this.provider.getOptional.bind(this.provider);
}
getTransformer(middlewares: TransformerMiddleware[] = []) {
return new Transformer({
schema: this.schema,
blobCRUD: this.workspace.blobSync,
docCRUD: {
create: (id: string) => this.workspace.createDoc({ id }),
get: (id: string) => this.workspace.getDoc(id),
delete: (id: string) => this.workspace.removeDoc(id),
},
middlewares,
});
}
}

View File

@@ -1,7 +1,5 @@
import type { Slot } from '@blocksuite/global/utils';
import type { Workspace } from './workspace.js';
export type Tag = {
id: string;
value: string;
@@ -38,8 +36,6 @@ export interface WorkspaceMeta {
get name(): string | undefined;
setName(name: string): void;
hasVersion: boolean;
writeVersion(workspace: Workspace): void;
get docs(): unknown[] | undefined;
initialize(): void;

Some files were not shown because too many files have changed in this diff Show More