Compare commits

...

31 Commits

Author SHA1 Message Date
zzj3720
0ef01002b4 fix(editor): slice to snapshot supports flat data structures 2025-02-27 15:29:17 +08:00
Saul-Mirone
b7598ff177 chore: optimize code 2025-02-27 12:57:09 +08:00
zzj3720
b07eb5678d fix(editor): toDraftModal supports flat data structures 2025-02-27 12:39:20 +08: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
darkskygit
842c39c3be feat(native): doc loader for common native (#9941) 2025-02-25 07:50:56 +00:00
EYHN
26674b0cb8 fix(core): fallback when loading share page (#10428) 2025-02-25 07:35:58 +00:00
EYHN
cafff4e0eb fix(nbstore): reduce unnecessary sync (#10426) 2025-02-25 07:21:46 +00:00
fundon
abc3f9f23f chore(editor): bump @floating-ui/dom to 1.6.13 (#10425) 2025-02-25 07:06:27 +00:00
Brooooooklyn
5dbffba08d feat(native): media capture (#9992) 2025-02-25 06:51:56 +00:00
272 changed files with 10550 additions and 1318 deletions

1326
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,11 +15,16 @@ affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
anyhow = "1"
base64-simd = "0.8"
block2 = "0.6"
chrono = "0.4"
core-foundation = "0.10"
coreaudio-rs = "0.12"
criterion2 = { version = "2", default-features = false }
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" }
@@ -31,6 +36,8 @@ once_cell = "1"
parking_lot = "0.12"
rand = "0.9"
rayon = "1.10"
rubato = "0.16"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"

View File

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -22,7 +22,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -21,7 +21,7 @@
"@blocksuite/icons": "^2.2.3",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.3",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -20,7 +20,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -22,7 +22,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -22,7 +22,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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

@@ -21,7 +21,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -21,7 +21,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -20,7 +20,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -20,7 +20,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -42,7 +42,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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

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

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

@@ -22,7 +22,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.1",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",

View File

@@ -30,7 +30,10 @@ export const tableBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
const tableProps = parseTableFromHtml(o.node);
const astToDelta = context.deltaConverter.astToDelta.bind(
context.deltaConverter
);
const tableProps = parseTableFromHtml(o.node, astToDelta);
walkerContext.openNode(
{
type: 'block',

View File

@@ -25,12 +25,15 @@ export const tableBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
enter: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
const astToDelta = context.deltaConverter.astToDelta.bind(
context.deltaConverter
);
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: TableModelFlavour,
props: parseTableFromMarkdown(o.node),
props: parseTableFromMarkdown(o.node, astToDelta),
children: [],
},
'children'

View File

@@ -7,6 +7,7 @@ import {
BlockPlainTextAdapterExtension,
type BlockPlainTextAdapterMatcher,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store';
import { createTableProps, formatTable, processTable } from './utils.js';
@@ -21,10 +22,14 @@ export const tableBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = {
const text = o.node.content;
const rowTexts = text.split('\n');
if (rowTexts.length <= 1) return;
const rowTextLists: string[][] = [];
const rowTextLists: DeltaInsert[][][] = [];
let columnCount: number | null = null;
for (const row of rowTexts) {
const cells = row.split('\t');
const cells = row.split('\t').map<DeltaInsert[]>(text => [
{
insert: text,
},
]);
if (cells.length <= 1) return;
if (columnCount == null) {
columnCount = cells.length;

View File

@@ -5,14 +5,23 @@ import type {
TableRow,
} from '@blocksuite/affine-model';
import {
AdapterTextUtils,
HastUtils,
type HtmlAST,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { HastUtils } from '@blocksuite/affine-shared/adapters';
import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store';
import type { Element, ElementContent } from 'hast';
import type { PhrasingContent, Table as MarkdownTable, TableCell } from 'mdast';
import type { Element } from 'hast';
import type { Table as MarkdownTable } from 'mdast';
type RichTextType = DeltaInsert[];
const createRichText = (text: RichTextType) => {
return {
'$blocksuite:internal:text$': true,
delta: text,
};
};
function calculateColumnWidths(rows: string[][]): number[] {
return (
rows[0]?.map((_, colIndex) =>
@@ -92,15 +101,6 @@ export const processTable = (
});
return table;
};
const getTextFromElement = (element: ElementContent): string => {
if (element.type === 'text') {
return element.value.trim();
}
if (element.type === 'element') {
return element.children.map(child => getTextFromElement(child)).join('');
}
return '';
};
const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
if (!node) {
@@ -120,7 +120,7 @@ const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
return [];
};
export const createTableProps = (rowTextLists: string[][]) => {
export const createTableProps = (deltasLists: RichTextType[][]) => {
const createIdAndOrder = (count: number) => {
const result: { id: string; order: string }[] = Array.from({
length: count,
@@ -135,8 +135,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
}
return result;
};
const columnCount = Math.max(...rowTextLists.map(row => row.length));
const rowCount = rowTextLists.length;
const columnCount = Math.max(...deltasLists.map(row => row.length));
const rowCount = deltasLists.length;
const columns: TableColumn[] = createIdAndOrder(columnCount).map(v => ({
columnId: v.id,
@@ -156,9 +156,9 @@ export const createTableProps = (rowTextLists: string[][]) => {
continue;
}
const cellId = `${row.rowId}:${column.columnId}`;
const text = rowTextLists[i]?.[j];
const text = deltasLists[i]?.[j];
cells[cellId] = {
text: AdapterTextUtils.createText(text ?? ''),
text: createRichText(text ?? []),
};
}
}
@@ -172,7 +172,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
};
export const parseTableFromHtml = (
element: Element
element: Element,
astToDelta: (ast: HtmlAST) => RichTextType
): TableBlockPropsSerialized => {
const headerRows = getAllTag(element, 'thead').flatMap(node =>
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'th'))
@@ -184,33 +185,26 @@ export const parseTableFromHtml = (
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td'))
);
const allRows = [...headerRows, ...bodyRows, ...footerRows];
const rowTextLists: string[][] = [];
const rowTextLists: RichTextType[][] = [];
allRows.forEach(cells => {
const row: string[] = [];
const row: RichTextType[] = [];
cells.forEach(cell => {
row.push(getTextFromElement(cell));
row.push(astToDelta(cell));
});
rowTextLists.push(row);
});
return createTableProps(rowTextLists);
};
const getTextFromTableCell = (node: TableCell) => {
const getTextFromPhrasingContent = (node: PhrasingContent) => {
if (node.type === 'text') {
return node.value;
}
return '';
};
return node.children.map(child => getTextFromPhrasingContent(child)).join('');
};
export const parseTableFromMarkdown = (node: MarkdownTable) => {
const rowTextLists: string[][] = [];
export const parseTableFromMarkdown = (
node: MarkdownTable,
astToDelta: (ast: MarkdownAST) => RichTextType
) => {
const rowTextLists: RichTextType[][] = [];
node.children.forEach(row => {
const rowText: string[] = [];
const rowText: RichTextType[] = [];
row.children.forEach(cell => {
rowText.push(getTextFromTableCell(cell));
rowText.push(astToDelta(cell));
});
rowTextLists.push(rowText);
});

View File

@@ -20,7 +20,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@lottiefiles/dotlottie-wc": "^0.4.0",
"@preact/signals-core": "^1.8.0",

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

@@ -20,7 +20,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/store": "workspace:*",
"@emotion/hash": "^0.9.2",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -3,17 +3,30 @@ import { propertyType } from '../../core/property/property-config.js';
export const checkboxPropertyType = propertyType('checkbox');
const FALSE_VALUES = new Set([
'false',
'no',
'0',
'',
'undefined',
'null',
'否',
'不',
'错',
'错误',
'取消',
'关闭',
]);
export const checkboxPropertyModelConfig =
checkboxPropertyType.modelConfig<boolean>({
name: 'Checkbox',
type: () => t.boolean.instance(),
defaultData: () => ({}),
cellToString: ({ value }) => (value ? 'True' : 'False'),
cellFromString: ({ value }) => {
return {
value: value !== 'False',
};
},
cellFromString: ({ value }) => ({
value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()),
}),
cellToJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) =>
typeof value !== 'boolean' ? undefined : value,

View File

@@ -23,7 +23,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

View File

@@ -22,7 +22,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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

@@ -19,7 +19,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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'
@@ -27,10 +30,12 @@ interface Tile {
// With high enough zoom, fallback to DOM rendering
const zoomThreshold = 1;
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,10 @@ 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.viewportUpdated.on(() => {
this.refresh().catch(console.error);
@@ -66,70 +73,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);
},
1000, // During this period, fallback to DOM
{ 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 +165,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 +176,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 +220,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 +247,54 @@ 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;
}
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');
}
canOptimize(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
}
private toggleOptimization(value: boolean) {
if (
this.viewportElement &&
this.viewportElement.enableOptimization !== value
) {
this.viewportElement.enableOptimization = value;
this.debugLog(`${value ? 'Enabled' : 'Disabled'} optimization`);
}
}
}

View File

@@ -25,7 +25,7 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",

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;
}

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