Compare commits

...

49 Commits

Author SHA1 Message Date
zzj3720 b15a2a9638 fix(editor): adjust the style of the table block 2025-02-21 17:10:06 +08:00
Saul-Mirone f3218ab3bc refactor(editor): rename presets to integration test (#10340) 2025-02-21 06:26:03 +00:00
L-Sun f79324b6a1 chore(editor): update shadow of ask-ai-panel (#10336)
Close [PD-2343](https://linear.app/affine-design/issue/PD-2343/[ui]-ai-面板-shadow-改为-overlaypanelshadow)
2025-02-21 06:12:05 +00:00
Saul-Mirone adcc6b578c refactor(editor): move editor components to frontend core (#10335)
### TL;DR
Moved editor components from BlockSuite presets to AFFiNE core and updated imports accordingly.

### What changed?
- Relocated `EdgelessEditor` and `PageEditor` components from BlockSuite presets to AFFiNE core
- Removed basic editor examples from playground
- Updated import paths across the codebase to reference new component locations
- Added editor effects registration in AFFiNE core
- Removed editor exports from BlockSuite presets

### How to test?
1. Launch the application
2. Verify both page and edgeless editors load correctly
3. Confirm editor functionality remains intact including:
   - Document editing
   - Mode switching
   - Editor toolbars and controls
   - Multiple editor instances

### Why make this change?
This change better aligns with AFFiNE's architecture by moving editor components closer to where they are used. It reduces coupling with BlockSuite presets and gives AFFiNE more direct control over editor customization and implementation.
2025-02-21 04:28:54 +00:00
fengmk2 7f833f8c15 fix(server): don't sync blob meta on workspace deleted event (#10334)
close CLOUD-128
2025-02-21 04:11:57 +00:00
JimmFly 785951bbfa fix(core): adjust copy link button styles (#10337)
close PD-2344
![CleanShot 2025-02-21 at 11 22 32@2x](https://github.com/user-attachments/assets/97d1052b-c900-47df-89ba-476787a28587)
![CleanShot 2025-02-21 at 11 22 08@2x](https://github.com/user-attachments/assets/2b455797-dfc8-4572-95ac-3ee8b33b0528)
2025-02-21 03:58:03 +00:00
fundon 19e9f970f4 fix(editor): block selected style in note under edgeless (#10326)
Related to: https://github.com/toeverything/AFFiNE/pull/9849

Currently missing selected style in note under edgeless.
<img width="860" alt="Screenshot 2025-02-20 at 20 51 12" src="https://github.com/user-attachments/assets/77d68cfb-13d0-4e09-a567-f2a30ba72db1" />
2025-02-21 03:43:54 +00:00
doodlewind c362737441 perf(editor): reduce dom ops in viewport update (#10333) 2025-02-21 03:30:10 +00:00
EYHN 618042812b fix(nbstore): fix cloud awareness (#10320) 2025-02-21 11:11:10 +08:00
fengmk2 0ff7c5f897 fix(server): gen new request id on websocket event request (#10330)
After

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/hTwOityLamd4hitrae7M/22431b9a-30e8-48a3-8db6-de377671f9b7.png)
2025-02-21 02:00:58 +00:00
Saul-Mirone b8dcb85007 refactor: move outline fragment to separate package (#10331)
### TL;DR

Moved outline functionality into a dedicated fragment package and updated vanilla-extract CSS dependency.

### What changed?

- Created new `@blocksuite/affine-fragment-outline` package
- Relocated outline-related code from presets to the new fragment package
- Updated imports across affected files to reference the new package location
- Upgraded `@vanilla-extract/css` dependency from 1.14.0/1.16.1 to 1.17.0
- Added necessary package configuration and TypeScript setup for the new fragment

### How to test?

1. Verify outline functionality works as expected in both desktop and mobile views
2. Check that outline panel, viewer, and mobile menu components render correctly
3. Ensure outline navigation and interactions continue to work
4. Confirm no regressions in outline-related features

### Why make this change?

This change improves code organization by isolating outline functionality into a dedicated package, following the modular architecture pattern. This makes the codebase more maintainable and allows for better separation of concerns. The vanilla-extract CSS upgrade ensures consistency across packages and provides access to the latest features and fixes.
2025-02-20 15:59:13 +00:00
Saul-Mirone 5ac15f12e6 refactor: replace editor container with editor host (#10328)
### TL;DR
Refactored editor access to use `EditorHost` instead of `AffineEditorContainer` and updated mode access through `DocModeProvider`.

### What changed?
- Changed editor property types from `AffineEditorContainer` to `EditorHost` across multiple components
- Updated mode access to use `DocModeProvider` service instead of direct editor mode access
- Modified editor references to use `editor.host` where appropriate
- Updated scroll and highlight utilities to work with `EditorHost`

### How to test?
1. Open a document in both page and edgeless modes
2. Verify outline panel functionality works as expected
3. Test outline viewer navigation and highlighting
4. Confirm mobile outline menu operates correctly
5. Check that frame panel and TOC features work in all modes

### Why make this change?
This change improves architectural consistency by using `EditorHost` directly and accessing mode through the proper service provider. This makes the code more maintainable and follows better dependency practices by using the correct abstraction levels.
2025-02-20 14:20:32 +00:00
akumatus efe36161e8 fix(core): remove candidate doc chip suggestions (#10327)
Fix issue [AF-2247](https://linear.app/affine-design/issue/AF-2247).
2025-02-20 14:01:00 +00:00
akumatus 126677d7ad fix(core): no pop-ups if user click discard menu item (#10317)
Fix issue [BS-2628](https://linear.app/affine-design/issue/BS-2628).
2025-02-20 13:29:28 +00:00
Saul-Mirone 007bbabce4 refactor: move frame manager and panel to separate packages (#10324)
### TL;DR
Moved frame management functionality from `blocksuite/blocks` to `@blocksuite/affine-block-frame` package.

### What changed?
- Relocated `frame-manager.ts` from `blocksuite/blocks` to `@blocksuite/affine-block-frame`
- Added new dependencies to block-frame package: `@blocksuite/affine-block-surface` and `yjs`
- Updated imports across multiple components to reference frame manager from its new location
- Moved utility functions `areSetsEqual` and `isFrameBlock` into frame-manager file
- Replaced direct EdgelessRootService references with GfxController in frame panel components

### How to test?
1. Verify frame functionality works in edgeless mode
2. Test frame creation, selection, and manipulation
3. Confirm frame navigation and presentation modes operate correctly
4. Check that frame panel and toolbar interactions remain functional

### Why make this change?
This refactoring improves code organization by consolidating frame-related functionality into a dedicated package, making the codebase more modular and easier to maintain. It also reduces dependencies between packages and provides clearer boundaries for frame-related features.
2025-02-20 13:06:40 +00:00
doodlewind 64cc99354e refactor(editor): add zoom threshold for dom rendering fallback (#10322) 2025-02-20 11:45:19 +00:00
Saul-Mirone 1516903c77 refactor: move doc-title and ai-chat-block components (#10316)
### TL;DR
Moved doc title and AI chat block components to more appropriate locations while removing unused backlink functionality.

### What changed?
- Relocated doc title component from presets to affine-components
- Moved AI chat block from presets/blocks to blocks directory
- Removed unused backlink-related code and components
- Updated imports across files to reference new component locations
- Consolidated AI-related exports through a single entry point

### How to test?
1. Verify doc title still renders correctly in documents
2. Confirm AI chat functionality works as expected
3. Check that no backlink-related features are accessible
4. Ensure all AI features continue to work through the new import paths

### Why make this change?
This reorganization improves code organization by:
- Placing components closer to their related functionality
- Removing dead/unused code around backlinks
- Simplifying the import structure for AI-related features
- Making the codebase more maintainable by consolidating related components
2025-02-20 10:45:47 +00:00
EYHN 4f831732e1 fix(core): fix throw if aborted polyfill (#10321) 2025-02-20 10:32:02 +00:00
zzj3720 ef28e36441 fix(editor): data in the database will be completely overwritten in some cases (#10318) 2025-02-20 10:17:57 +00:00
doodlewind 7b1dfb7ee8 refactor(editor): reduce dom query per refresh (#10319) 2025-02-20 10:01:13 +00:00
Hwang 5fcc402280 style: update ios dark icon (#10312) 2025-02-20 08:01:32 +00:00
DarkSky fa86f71853 feat(server): client version check (#9205)
Co-authored-by: forehalo <forehalo@gmail.com>
2025-02-20 15:50:22 +08:00
L-Sun 4fee2a9c4b chore(editor): update some widget styles (#10311) 2025-02-20 15:39:48 +08:00
fengmk2 b4097aef8e refactor(server): move bin content parser to doc reader (#10302) 2025-02-20 07:19:48 +00:00
Saul-Mirone 9f4311f654 refactor(editor): remove AbstractEditor type and feature flags in test (#10308) 2025-02-20 07:05:35 +00:00
darkskygit 13f1859cdf feat: allow retry with new message (#10307)
fix AF-1630
2025-02-20 06:07:53 +00:00
Flrande 50820482df fix(editor): auto focus after add inline latex (#10309)
https://github.com/user-attachments/assets/09e713ee-e600-464c-8614-d874d343cfb0

Close #10208
2025-02-20 05:52:06 +00:00
forehalo ec67d30b27 chore(server): race condition during fixing doc owner (#10303) 2025-02-20 05:34:55 +00:00
forehalo fd5897dbe6 chore(server): disable nightly subscriptions expirasion check (#10298) 2025-02-20 05:34:54 +00:00
liuyi d490e767eb fix(server): wrong previous subscription check (#10306) 2025-02-20 13:33:49 +08:00
fundon adc003862b fix(editor): image size and xywh when converting attachment to image (#10200)
In Edgeless, the image size should be read when converting attachment to image:

* fix `size`
* fix `xywh`
2025-02-20 05:16:21 +00:00
fengmk2 ff0ce1a962 fix(server): remove job on complete (#10305) 2025-02-20 04:39:05 +00:00
EYHN 5042d9f644 fix(nbstore): check before save empty update (#10304) 2025-02-20 04:24:50 +00:00
JimmFly 1d339c682b fix(core): adjust share menu and upgrade-to-team page style (#10299)
close PD-2330 PD-2331 AF-2238
2025-02-20 04:10:52 +00:00
doodlewind b38abcb59c perf(editor): avoid recreation of Intl.Segmenter (#10295)
<img width="537" alt="image" src="https://github.com/user-attachments/assets/43e54d94-6228-4c26-b3d0-cf4725b251e4" />
2025-02-20 03:55:57 +00:00
Saul-Mirone c3fc0a0d88 chore(editor): fix imports in legacy tests (#10300) 2025-02-20 03:30:05 +00:00
fengmk2 e0b2b2b33c fix(server): convert error type to lower case (#10301) 2025-02-20 03:04:21 +00:00
zzj3720 ba91b36cc3 feat(editor): add block creation tracking (#10294)
fix: PD-2090
2025-02-20 02:25:29 +00:00
github-actions[bot] a0e3f9909c chore(i18n): sync translations (#10184)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-20 02:25:19 +00:00
EYHN 8f6ce2c3b4 fix(nbstore): fix doc clock comparison (#10296) 2025-02-20 10:10:17 +08:00
zzj3720 d8435421d2 fix(editor): improve table selection in edgeless mode (#10293)
fix: BS-2614, BS-2560
2025-02-20 01:50:40 +00:00
doodlewind 676ccc9094 refactor(editor): allow force refresh in worker renderer (#10289) 2025-02-19 14:23:45 +00:00
Saul-Mirone 091ba7bb51 chore: remove createEmptyEditor legacy test misc (#10291) 2025-02-19 21:38:00 +08:00
donteatfriedrice 2b11941c0e chore: bump theme (#10290) 2025-02-19 11:37:19 +00:00
forehalo 02f567f2c0 chore(core): better doc action error message (#10288) 2025-02-19 10:38:38 +00:00
donteatfriedrice 54b7515167 fix(editor): log when export blob failed (#10287) 2025-02-19 10:06:50 +00:00
CatsJuice e726df9a1b fix(core): prevent all-docs header from overlapping scrollbar (#10270) 2025-02-19 09:24:49 +00:00
CatsJuice 926b35c91f chore(core): temporarily hide editor starter-bar on mobile (#10237) 2025-02-19 09:10:26 +00:00
JimmFly b456feee63 fix(core): unexpected redirect to expired page after accepting invitation (#10257)
Co-authored-by: EYHN <cneyhn@gmail.com>
2025-02-19 09:10:12 +00:00
540 changed files with 7068 additions and 2816 deletions
-5
View File
@@ -17,7 +17,6 @@
"@blocksuite/blocks": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/presets": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*"
},
@@ -37,7 +36,6 @@
"./inline": "./src/inline/index.ts",
"./inline/consts": "./src/inline/consts.ts",
"./inline/types": "./src/inline/types.ts",
"./presets": "./src/presets/index.ts",
"./blocks": "./src/blocks/index.ts",
"./blocks/schemas": "./src/blocks/schemas.ts",
"./sync": "./src/sync/index.ts"
@@ -83,9 +81,6 @@
"inline/types": [
"dist/inline/types.d.ts"
],
"presets": [
"dist/presets/index.d.ts"
],
"blocks": [
"dist/blocks/index.d.ts"
],
-2
View File
@@ -1,7 +1,5 @@
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
export function effects() {
blocksEffects();
presetsEffects();
}
@@ -1 +0,0 @@
export * from '@blocksuite/presets';
-1
View File
@@ -11,7 +11,6 @@
{ "path": "../../blocks" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
{ "path": "../../presets" },
{ "path": "../../framework/store" },
{ "path": "../../framework/sync" }
]
@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -38,8 +38,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
protected _isResizing = false;
protected _isSelected = false;
protected _whenHover: HoverController | null = new HoverController(
this,
({ abortController }) => {
@@ -35,7 +35,7 @@ export class AttachmentEdgelessBlockComponent extends toGfxBlockComponent(
this.slots.elementResizeEnd.on(() => {
this._isResizing = false;
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
this._isResizing || this._isDragging || !this.selected$.peek();
})
);
}
+46 -11
View File
@@ -1,20 +1,25 @@
import type {
AttachmentBlockModel,
ImageBlockProps,
import {
type AttachmentBlockModel,
type ImageBlockProps,
MAX_IMAGE_WIDTH,
} from '@blocksuite/affine-model';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import {
readImageSize,
transformModel,
withTempBlobData,
} from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
import type { Container } from '@blocksuite/global/di';
import { createIdentifier } from '@blocksuite/global/di';
import { Bound } from '@blocksuite/global/utils';
import type { ExtensionType } from '@blocksuite/store';
import { Extension } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { getAttachmentBlob } from './utils';
export type AttachmentEmbedConfig = {
name: string;
/**
@@ -24,7 +29,10 @@ export type AttachmentEmbedConfig = {
/**
* The action will be executed when the 「Turn into embed view」 button is clicked.
*/
action?: (model: AttachmentBlockModel) => Promise<void> | void;
action?: (
model: AttachmentBlockModel,
std: BlockStdScope
) => Promise<void> | void;
/**
* The template will be used to render the embed view.
*/
@@ -89,11 +97,11 @@ export class AttachmentEmbedService extends Extension {
// Converts to embed view.
convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
const config = this.values.find(config => config.check(model, maxFileSize));
if (!config || !config.action) {
if (!config?.action) {
model.doc.updateBlock(model, { embed: true });
return;
}
config.action(model)?.catch(console.error);
config.action(model, this.std)?.catch(console.error);
}
embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
@@ -123,7 +131,12 @@ const embedConfig: AttachmentEmbedConfig[] = [
check: model =>
model.doc.schema.flavourSchemaMap.has('affine:image') &&
model.type.startsWith('image/'),
action: model => turnIntoImageBlock(model),
async action(model, std) {
const component = std.view.getBlock(model.id);
if (!component) return;
await turnIntoImageBlock(model);
},
},
{
name: 'pdf',
@@ -171,7 +184,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
/**
* Turn the attachment block into an image block.
*/
export function turnIntoImageBlock(model: AttachmentBlockModel) {
export async function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!');
return;
@@ -183,15 +196,37 @@ export function turnIntoImageBlock(model: AttachmentBlockModel) {
const { saveAttachmentData, getImageData } = withTempBlobData();
saveAttachmentData(sourceId, { name: model.name });
const imageConvertData = model.sourceId
? getImageData(model.sourceId)
let imageSize = model.sourceId ? getImageData(model.sourceId) : undefined;
const bounds = model.xywh
? Bound.fromXYWH(model.deserializedXYWH)
: undefined;
if (bounds) {
if (!imageSize?.width || !imageSize?.height) {
const blob = await getAttachmentBlob(model);
if (blob) {
imageSize = await readImageSize(blob);
}
}
if (imageSize?.width && imageSize?.height) {
const p = imageSize.height / imageSize.width;
imageSize.width = Math.min(imageSize.width, MAX_IMAGE_WIDTH);
imageSize.height = imageSize.width * p;
bounds.w = imageSize.width;
bounds.h = imageSize.height;
}
}
const others = bounds ? { xywh: bounds.serialize() } : undefined;
const imageProp: Partial<ImageBlockProps> = {
sourceId,
caption: model.caption,
size: model.size,
...imageConvertData,
...imageSize,
...others,
};
transformModel(model, 'affine:image', imageProp);
}
@@ -97,7 +97,7 @@ export async function uploadAttachmentBlob(
}
}
async function getAttachmentBlob(model: AttachmentBlockModel) {
export async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.sourceId;
if (!sourceId) {
return null;
@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -4,9 +4,10 @@ import {
} from '@blocksuite/affine-components/caption';
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { refreshBookmarkUrlData } from './utils.js';
@@ -14,6 +15,12 @@ import { refreshBookmarkUrlData } from './utils.js';
export const BOOKMARK_MIN_WIDTH = 450;
export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBlockModel> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
() => ({
'selected-style': this.selected$.value,
})
);
private _fetchAbortController?: AbortController;
blockDraggable = true;
@@ -69,16 +76,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
override renderBlock() {
const selected = this.selected$.value;
const isInEdgeless =
this.std.get(DocModeProvider).getEditorMode() === 'edgeless';
return html`
<div
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'affine-bookmark-container': true,
'selected-style': selected && !isInEdgeless,
...this.selectedStyle$?.value,
})}
style=${this.containerStyleMap}
>
@@ -10,6 +10,8 @@ import { BookmarkBlockComponent } from './bookmark-block.js';
export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
BookmarkBlockComponent
) {
override selectedStyle$ = null;
override blockDraggable = false;
override getRenderingRect() {
+1 -1
View File
@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
@@ -34,8 +34,6 @@ import {
} from './properties/index.js';
import {
addProperty,
applyCellsUpdate,
applyPropertyUpdate,
copyCellsByProperty,
deleteRows,
deleteView,
@@ -169,7 +167,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
columnId: propertyId,
value: newValue,
});
applyCellsUpdate(this._model);
}
}
@@ -199,7 +196,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
insertToPosition,
property.create(this.newPropertyName())
);
applyPropertyUpdate(this._model);
return result;
}
@@ -283,7 +279,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
propertyDataSet(propertyId: string, data: Record<string, unknown>): void {
this._runCapture();
this.updateProperty(propertyId, () => ({ data }));
applyPropertyUpdate(this._model);
}
propertyDataTypeGet(propertyId: string): TypeInstance | undefined {
@@ -337,7 +332,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
schema
);
copyCellsByProperty(this._model, copyId, id);
applyPropertyUpdate(this._model);
return id;
}
@@ -366,7 +360,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
propertyNameSet(propertyId: string, name: string): void {
this.doc.captureSync();
this.updateProperty(propertyId, () => ({ name }));
applyPropertyUpdate(this._model);
}
override propertyReadonlyGet(propertyId: string): boolean {
@@ -421,7 +414,6 @@ export class DatabaseBlockDataSource extends DataSourceBase {
}
});
updateCells(this._model, propertyId, cells);
applyPropertyUpdate(this._model);
}
rowAdd(insertPosition: InsertToPosition | number): string {
@@ -37,24 +37,6 @@ export function addProperty(
return id;
}
export function applyCellsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
cells: model.cells,
});
}
export function applyPropertyUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
columns: model.columns,
});
}
export function applyViewsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
views: model.views,
});
}
export function copyCellsByProperty(
model: DatabaseBlockModel,
fromId: Column['id'],
@@ -156,7 +138,6 @@ export function moveViewTo(
arr => insertPositionToIndex(position, arr)
);
});
applyViewsUpdate(model);
}
export function updateCell(
@@ -255,6 +236,5 @@ export const updateView = <ViewData extends ViewBasicDataType>(
return { ...v, ...update(v as ViewData) };
});
});
applyViewsUpdate(model);
};
export const DATABASE_CONVERT_WHITE_LIST = ['affine:list', 'affine:paragraph'];
+1 -1
View File
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
+1 -1
View File
@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"yjs": "^13.6.21",
@@ -12,10 +12,11 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services';
import type { BlockService } from '@blocksuite/block-std';
import type { GfxCompatibleProps } from '@blocksuite/block-std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
export class EmbedBlockComponent<
@@ -23,6 +24,12 @@ export class EmbedBlockComponent<
Service extends BlockService = BlockService,
WidgetName extends string = string,
> extends CaptionedBlockComponent<Model, Service, WidgetName> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
() => ({
'selected-style': this.selected$.value,
})
);
private _fetchAbortController = new AbortController();
_cardStyle: EmbedCardStyle = 'horizontal';
@@ -43,10 +50,6 @@ export class EmbedBlockComponent<
protected embedContainerStyle: StyleInfo = {};
renderEmbed = (content: () => TemplateResult) => {
const selected = this.selected$.value;
const isInEdgeless =
this.std.get(DocModeProvider).getEditorMode() === 'edgeless';
if (
this._cardStyle === 'horizontal' ||
this._cardStyle === 'horizontalThin' ||
@@ -54,7 +57,7 @@ export class EmbedBlockComponent<
) {
this.style.display = 'block';
if (isInEdgeless) {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
this.style.minWidth = `${EMBED_CARD_MIN_WIDTH}px`;
}
}
@@ -64,7 +67,7 @@ export class EmbedBlockComponent<
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'embed-block-container': true,
'selected-style': selected && !isInEdgeless,
...this.selectedStyle$?.value,
})}
style=${styleMap({
height: `${this._cardHeight}px`,
@@ -22,6 +22,8 @@ export function toEdgelessEmbedBlock<
B extends typeof EmbedBlockComponent<Model, Service, WidgetName>,
>(block: B) {
return class extends toGfxBlockComponent(block) {
override selectedStyle$ = null;
_isDragging = false;
_isResizing = false;
+3 -1
View File
@@ -13,6 +13,7 @@
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
@@ -23,10 +24,11 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"exports": {
@@ -21,14 +21,13 @@ import {
type IVec,
type SerializedXYWH,
} from '@blocksuite/global/utils';
import { Text } from '@blocksuite/store';
import { type BlockModel, Text } from '@blocksuite/store';
import * as Y from 'yjs';
import { areSetsEqual } from './utils/misc.js';
import { isFrameBlock } from './utils/query.js';
const FRAME_PADDING = 40;
export type NavigatorMode = 'fill' | 'fit';
export class FrameOverlay extends Overlay {
static override overlayName: string = 'frame';
@@ -461,3 +460,13 @@ export class EdgelessFrameManager extends GfxExtension {
this._disposable.dispose();
}
}
function areSetsEqual<T>(setA: Set<T>, setB: Set<T>) {
if (setA.size !== setB.size) return false;
for (const a of setA) if (!setB.has(a)) return false;
return true;
}
export function isFrameBlock(element: unknown): element is FrameBlockModel {
return !!element && (element as BlockModel).flavour === 'affine:frame';
}
@@ -1,2 +1,3 @@
export * from './frame-block.js';
export * from './frame-manager.js';
export * from './frame-spec.js';
@@ -7,6 +7,7 @@
},
"include": ["./src"],
"references": [
{ "path": "../block-surface" },
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
+1 -1
View File
@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
+1 -21
View File
@@ -12,6 +12,7 @@ import {
import {
downloadBlob,
humanFileSize,
readImageSize,
transformModel,
withTempBlobData,
} from '@blocksuite/affine-shared/utils';
@@ -425,27 +426,6 @@ export async function turnImageIntoCardView(
transformModel(model, 'affine:attachment', attachmentProp);
}
export function readImageSize(file: File | Blob) {
return new Promise<{ width: number; height: number }>(resolve => {
const size = { width: 0, height: 0 };
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
URL.revokeObjectURL(img.src);
resolve(size);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
resolve(size);
};
img.src = URL.createObjectURL(file);
});
}
export async function addImages(
std: BlockStdScope,
files: File[],
+1 -1
View File
@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/katex": "^0.16.7",
"@types/mdast": "^4.0.4",
"katex": "^0.16.11",
+1 -1
View File
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
+1 -1
View File
@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"fractional-indexing": "^3.2.0",
"lit": "^3.2.0",
"lodash.chunk": "^4.2.0",
+1 -1
View File
@@ -22,7 +22,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"fractional-indexing": "^3.2.0",
"html2canvas": "^1.4.1",
"lit": "^3.2.0",
+1 -1
View File
@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@preact/signals-core": "^1.8.0",
"@vanilla-extract/css": "^1.14.0",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"yjs": "^13.6.21",
"zod": "^3.24.1"
@@ -43,6 +43,10 @@ export class SelectionController implements ReactiveController {
private get clipboard() {
return this.host.std.clipboard;
}
private get scale() {
return this.host.getScale();
}
widthAdjust(dragHandle: HTMLElement, event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
@@ -50,6 +54,7 @@ export class SelectionController implements ReactiveController {
const currentWidth =
dragHandle.closest('td')?.getBoundingClientRect().width ??
DefaultColumnWidth;
const adjustedWidth = currentWidth / this.scale;
const columnId = dragHandle.dataset['widthAdjustColumnId'];
if (!columnId) {
return;
@@ -60,7 +65,7 @@ export class SelectionController implements ReactiveController {
columnId,
width: Math.max(
ColumnMinWidth,
event.clientX - initialX + currentWidth
(event.clientX - initialX) / this.scale + adjustedWidth
),
};
};
@@ -68,6 +68,7 @@ export class SelectionLayer extends SignalWatcher(
display: 'none',
});
const border = '2px solid var(--affine-primary-color)';
return styleMap({
position: 'absolute',
pointerEvents: 'none',
@@ -2,8 +2,7 @@ import { style } from '@vanilla-extract/css';
export const tableContainer = style({
display: 'block',
backgroundColor: 'var(--affine-background-primary-color)',
padding: '10px 0 18px',
padding: '10px 0 18px 10px',
overflowX: 'auto',
overflowY: 'visible',
selectors: {
@@ -54,6 +54,12 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
table$ = signal<HTMLTableElement>();
public getScale(): number {
const table = this.table$.value;
if (!table) return 1;
return table.getBoundingClientRect().width / table.offsetWidth;
}
private readonly getRootRect = () => {
const table = this.table$.value;
if (!table) return;
@@ -65,11 +71,12 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
const rootRect = this.getRootRect();
if (!row || !rootRect) return;
const rect = row.getBoundingClientRect();
const scale = this.getScale();
return {
top: rect.top - rootRect.top,
left: rect.left - rootRect.left,
width: rect.width,
height: rect.height,
top: (rect.top - rootRect.top) / scale,
left: (rect.left - rootRect.left) / scale,
width: rect.width / scale,
height: rect.height / scale,
};
};
@@ -80,11 +87,12 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
const firstRect = columns.item(0)?.getBoundingClientRect();
const lastRect = columns.item(columns.length - 1)?.getBoundingClientRect();
if (!firstRect || !lastRect) return;
const scale = this.getScale();
return {
top: firstRect.top - rootRect.top,
left: firstRect.left - rootRect.left,
width: firstRect.width,
height: lastRect.bottom - firstRect.top,
top: (firstRect.top - rootRect.top) / scale,
left: (firstRect.left - rootRect.left) / scale,
width: firstRect.width / scale,
height: (lastRect.bottom - firstRect.top) / scale,
};
};
@@ -99,19 +107,22 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
const startRow = rows.item(rowStartIndex);
const endRow = rows.item(rowEndIndex);
if (!startRow || !endRow || !rootRect) return;
const columns = startRow.querySelectorAll('td');
const startColumn = columns.item(columnStartIndex);
const endColumn = columns.item(columnEndIndex);
if (!startColumn || !endColumn) return;
const startRect = startRow.getBoundingClientRect();
const endRect = endRow.getBoundingClientRect();
const startColumnRect = startColumn.getBoundingClientRect();
const endColumnRect = endColumn.getBoundingClientRect();
const startCells = startRow.querySelectorAll('td');
const endCells = endRow.querySelectorAll('td');
const startCell = startCells.item(columnStartIndex);
const endCell = endCells.item(columnEndIndex);
if (!startCell || !endCell) return;
const startRect = startCell.getBoundingClientRect();
const endRect = endCell.getBoundingClientRect();
const scale = this.getScale();
return {
top: startRect.top - rootRect.top,
left: startColumnRect.left - rootRect.left,
width: endColumnRect.right - startColumnRect.left,
height: endRect.bottom - startRect.top,
top: (startRect.top - rootRect.top) / scale,
left: (startRect.left - rootRect.left) / scale,
width: (endRect.right - startRect.left) / scale,
height: (endRect.bottom - startRect.top) / scale,
};
};
@@ -124,7 +135,7 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
contenteditable="false"
class=${tableContainer}
style=${styleMap({
marginLeft: `-${virtualPadding}px`,
marginLeft: `-${virtualPadding + 10}px`,
marginRight: `-${virtualPadding}px`,
position: 'relative',
})}
@@ -4,7 +4,8 @@ import { createVar, style } from '@vanilla-extract/css';
export const cellContainerStyle = style({
position: 'relative',
alignItems: 'center',
border: '1px solid var(--affine-border-color)',
border: '1px solid',
borderColor: cssVarV2.table.border,
borderCollapse: 'collapse',
isolation: 'auto',
textAlign: 'start',
+1 -1
View File
@@ -24,7 +24,7 @@
"@lit/context": "^1.1.2",
"@lottiefiles/dotlottie-wc": "^0.4.0",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"collapse-white-space": "^2.1.0",
@@ -229,6 +229,10 @@ export class AffineLatexNode extends SignalWatcher(
latex,
}
);
this.editor.setInlineRange({
index: this.endOffset,
length: 0,
});
}
},
{ once: true }
+1 -1
View File
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"yjs": "^13.6.21",
@@ -0,0 +1,45 @@
{
"name": "@blocksuite/affine-fragment-frame-panel",
"description": "Frame panel fragment for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-frame": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}
@@ -1,12 +1,14 @@
import { type EditorHost, ShadowlessElement } from '@blocksuite/block-std';
import { generateKeyBetweenV2 } from '@blocksuite/block-std/gfx';
import { EdgelessFrameManager } from '@blocksuite/affine-block-frame';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
EdgelessFrameManager,
EdgelessRootService,
EditPropsStore,
type FrameBlockModel,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-shared/services';
import { type EditorHost, ShadowlessElement } from '@blocksuite/block-std';
import {
generateKeyBetweenV2,
GfxControllerIdentifier,
} from '@blocksuite/block-std/gfx';
import {
Bound,
DisposableGroup,
@@ -110,7 +112,7 @@ export class FramePanelBody extends SignalWatcher(
}
this._selected = [];
this._edgelessRootService?.selection.set({
this._gfx.selection.set({
elements: this._selected,
editing: false,
});
@@ -126,6 +128,10 @@ export class FramePanelBody extends SignalWatcher(
private _lastEdgelessRootId = '';
private get _gfx() {
return this.editorHost.std.get(GfxControllerIdentifier);
}
private readonly _selectFrame = (e: SelectEvent) => {
const { selected, id, multiselect } = e.detail;
@@ -138,7 +144,7 @@ export class FramePanelBody extends SignalWatcher(
this._selected = [id];
}
this._edgelessRootService?.selection.set({
this._gfx.selection.set({
elements: this._selected,
editing: false,
});
@@ -152,10 +158,6 @@ export class FramePanelBody extends SignalWatcher(
}));
};
get _edgelessRootService() {
return this.editorHost.std.getOptional(EdgelessRootService);
}
get frames() {
const frames = this.editorHost.doc
.getBlocksByFlavour('affine:frame')
@@ -229,8 +231,9 @@ export class FramePanelBody extends SignalWatcher(
private _fitToElement(e: FitViewEvent) {
const { block } = e.detail;
const bound = Bound.deserialize(block.xywh);
const docModeProvider = this.editorHost.std.get(DocModeProvider);
if (!this._edgelessRootService) {
if (docModeProvider.getEditorMode() !== 'edgeless') {
// When click frame card in page mode
// Should switch to edgeless mode and set viewport to the frame
const viewport = {
@@ -242,11 +245,7 @@ export class FramePanelBody extends SignalWatcher(
this.editorHost.std.get(EditPropsStore).setStorage('viewport', viewport);
this.editorHost.std.get(DocModeProvider).setEditorMode('edgeless');
} else {
this._edgelessRootService.viewport.setViewportByBound(
bound,
this.viewportPadding,
true
);
this._gfx.viewport.setViewportByBound(bound, this.viewportPadding, true);
}
}
@@ -398,7 +397,7 @@ export class FramePanelBody extends SignalWatcher(
this._setDocDisposables(this.editorHost.doc);
// after switch to edgeless mode, should update the selection
if (this.editorHost.doc.id === this._lastEdgelessRootId) {
this._edgelessRootService?.selection.set({
this._gfx.selection.set({
elements: this._selected,
editing: false,
});
@@ -1,5 +1,6 @@
import type { RichText } from '@blocksuite/affine-components/rich-text';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { ShadowlessElement } from '@blocksuite/block-std';
import type { FrameBlockModel, RichText } from '@blocksuite/blocks';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html } from 'lit';
import { property, query } from 'lit/decorators.js';
@@ -1,5 +1,5 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { ShadowlessElement } from '@blocksuite/block-std';
import type { FrameBlockModel } from '@blocksuite/blocks';
import { DisposableGroup, WithDisposable } from '@blocksuite/global/utils';
import { css, html, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
@@ -1,5 +1,6 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { on, once } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { type FrameBlockModel, on, once } from '@blocksuite/blocks';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, nothing } from 'lit';
import { property, query } from 'lit/decorators.js';
@@ -0,0 +1,32 @@
import {
AFFINE_FRAME_PANEL_BODY,
FramePanelBody,
} from './body/frame-panel-body';
import { AFFINE_FRAME_CARD, FrameCard } from './card/frame-card';
import {
AFFINE_FRAME_CARD_TITLE,
FrameCardTitle,
} from './card/frame-card-title';
import {
AFFINE_FRAME_TITLE_EDITOR,
FrameCardTitleEditor,
} from './card/frame-card-title-editor';
import { AFFINE_FRAME_PANEL, FramePanel } from './frame-panel';
import {
AFFINE_FRAME_PANEL_HEADER,
FramePanelHeader,
} from './header/frame-panel-header';
import {
AFFINE_FRAMES_SETTING_MENU,
FramesSettingMenu,
} from './header/frames-setting-menu';
export function effects() {
customElements.define(AFFINE_FRAME_PANEL, FramePanel);
customElements.define(AFFINE_FRAME_TITLE_EDITOR, FrameCardTitleEditor);
customElements.define(AFFINE_FRAME_CARD, FrameCard);
customElements.define(AFFINE_FRAME_CARD_TITLE, FrameCardTitle);
customElements.define(AFFINE_FRAME_PANEL_BODY, FramePanelBody);
customElements.define(AFFINE_FRAME_PANEL_HEADER, FramePanelHeader);
customElements.define(AFFINE_FRAMES_SETTING_MENU, FramesSettingMenu);
}
@@ -1,12 +1,12 @@
import type { EditorHost } from '@blocksuite/block-std';
import type { NavigatorMode } from '@blocksuite/affine-block-frame';
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import {
createButtonPopper,
DocModeProvider,
EdgelessLegacySlotIdentifier,
EdgelessRootService,
EditPropsStore,
type NavigatorMode,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-shared/services';
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { DisposableGroup, WithDisposable } from '@blocksuite/global/utils';
import { PresentationIcon, SettingsIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, type PropertyValues } from 'lit';
@@ -112,13 +112,18 @@ export class FramePanelHeader extends WithDisposable(LitElement) {
private _edgelessDisposables: DisposableGroup | null = null;
private get _gfx() {
return this.editorHost.std.get(GfxControllerIdentifier);
}
private readonly _enterPresentationMode = () => {
if (!this._edgelessRootService) {
const docModeProvider = this.editorHost.std.get(DocModeProvider);
if (docModeProvider.getEditorMode() !== 'edgeless') {
this.editorHost.std.get(DocModeProvider).setEditorMode('edgeless');
}
setTimeout(() => {
this._edgelessRootService?.gfx.tool.setTool({
this._gfx.tool.setTool({
type: 'frameNavigator',
mode: this._navigatorMode,
});
@@ -132,8 +137,6 @@ export class FramePanelHeader extends WithDisposable(LitElement) {
private _navigatorMode: NavigatorMode = 'fit';
private readonly _setEdgelessDisposables = () => {
if (!this._edgelessRootService) return;
const slots = this.editorHost.std.get(EdgelessLegacySlotIdentifier);
this._clearEdgelessDisposables();
@@ -145,10 +148,6 @@ export class FramePanelHeader extends WithDisposable(LitElement) {
);
};
private get _edgelessRootService() {
return this.editorHost.std.getOptional(EdgelessRootService);
}
private _tryLoadNavigatorStateLocalRecord() {
this._navigatorMode = this.editorHost.std
.get(EditPropsStore)
@@ -219,7 +218,8 @@ export class FramePanelHeader extends WithDisposable(LitElement) {
override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('editorHost')) {
if (this._edgelessRootService) {
const docModeProvider = this.editorHost.std.get(DocModeProvider);
if (docModeProvider.getEditorMode() === 'edgeless') {
this._setEdgelessDisposables();
} else {
this._clearEdgelessDisposables();
@@ -1,9 +1,9 @@
import type { EditorHost } from '@blocksuite/block-std';
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import {
EdgelessLegacySlotIdentifier,
EdgelessRootService,
DocModeProvider,
EditPropsStore,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, LitElement, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
@@ -103,10 +103,6 @@ export class FramesSettingMenu extends WithDisposable(LitElement) {
this._editPropsStore.setStorage('presentHideToolbar', this.hideToolbar);
};
private get _edgelessRootService() {
return this.editorHost.std.getOptional(EdgelessRootService);
}
private get _editPropsStore() {
return this.editorHost.std.get(EditPropsStore);
}
@@ -176,7 +172,8 @@ export class FramesSettingMenu extends WithDisposable(LitElement) {
override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('editorHost')) {
if (this._edgelessRootService) {
const docModeProvider = this.editorHost.std.get(DocModeProvider);
if (docModeProvider.getEditorMode() === 'edgeless') {
this.disposables.add(
this.slots.navigatorSettingUpdated.on(
({ blackBackground, hideToolbar }) => {
@@ -0,0 +1,2 @@
export * from './frame-panel';
export * from './tool';
@@ -1,7 +1,6 @@
import type { NavigatorMode } from '@blocksuite/affine-block-frame';
import { BaseTool } from '@blocksuite/block-std/gfx';
import type { NavigatorMode } from '../../../_common/edgeless/frame/consts.js';
type PresentToolOption = {
mode?: NavigatorMode;
};
@@ -1,4 +1,5 @@
import { type FrameBlockModel, on, once } from '@blocksuite/blocks';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { on, once } from '@blocksuite/affine-shared/utils';
import type { FramePanelBody } from '../body/frame-panel-body.js';
import { FrameCard } from '../card/frame-card.js';
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../block-frame" },
{ "path": "../block-surface" },
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
{ "path": "../../framework/block-std" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
{ "path": "../../framework/store" }
]
}
@@ -0,0 +1,45 @@
{
"name": "@blocksuite/affine-fragment-outline",
"description": "Outline fragment for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-note": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}
@@ -1,17 +1,10 @@
import { effects } from '@blocksuite/affine-block-note/effects';
import { changeNoteDisplayMode } from '@blocksuite/affine-block-note';
import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std';
import {
changeNoteDisplayMode,
matchModels,
NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/blocks';
import {
Bound,
noop,
SignalWatcher,
WithDisposable,
} from '@blocksuite/global/utils';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import { effect, signal } from '@preact/signals-core';
@@ -21,8 +14,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { when } from 'lit/directives/when.js';
noop(effects);
import { type TocContext, tocContext } from '../config';
import type {
ClickBlockEvent,
@@ -78,10 +69,6 @@ export class OutlinePanelBody extends SignalWatcher(
return this.editor.doc;
}
private get edgeless() {
return this.editor.querySelector('affine-edgeless-root');
}
get viewportPadding(): [number, number, number, number] {
const fitPadding = this._context.fitPadding$.value;
return fitPadding.length === 4
@@ -92,9 +79,9 @@ export class OutlinePanelBody extends SignalWatcher(
}
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
if (!this.edgeless) return;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const selection = gfx.selection;
const { selection } = this.edgeless.service;
if (!selection.has(note.id)) return;
const selectedIds = selection.selectedIds.filter(id => id !== note.id);
selection.set({
@@ -115,18 +102,12 @@ export class OutlinePanelBody extends SignalWatcher(
}
private _fitToElement(e: FitViewEvent) {
const edgeless = this.edgeless;
if (!edgeless) return;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const { block } = e.detail;
const bound = Bound.deserialize(block.xywh);
edgeless.service.viewport.setViewportByBound(
bound,
this.viewportPadding,
true
);
gfx.viewport.setViewportByBound(bound, this.viewportPadding, true);
}
// when display mode change to page only, we should de-select the note if it is selected in edgeless mode
@@ -199,6 +180,8 @@ export class OutlinePanelBody extends SignalWatcher(
private _selectNote(e: SelectEvent) {
const { selected, id, multiselect } = e.detail;
const gfx = this.editor.std.get(GfxControllerIdentifier);
const editorMode = this.editor.std.get(DocModeProvider).getEditorMode();
const note = this.doc.getBlock(id)?.model;
if (!note || !matchModels(note, [NoteBlockModel])) return;
@@ -212,8 +195,8 @@ export class OutlinePanelBody extends SignalWatcher(
selectedNotes = [note];
}
if (this.edgeless) {
this.edgeless?.service.selection.set({
if (editorMode === 'edgeless') {
gfx.selection.set({
elements: selectedNotes.map(({ id }) => id),
editing: false,
});
@@ -224,7 +207,9 @@ export class OutlinePanelBody extends SignalWatcher(
private _watchSelectedNotes() {
return effect(() => {
const { std, doc, mode } = this.editor;
const { std, doc } = this.editor;
const docModeService = this.editor.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
if (mode !== 'edgeless') return;
const currSelectedNotes = std.selection
@@ -1,9 +1,6 @@
import { type NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import {
createButtonPopper,
type NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { ArrowDownSmallIcon, InvisibleIcon } from '@blocksuite/icons/lit';
import type { BlockModel } from '@blocksuite/store';
@@ -1,16 +1,16 @@
import type {
AttachmentBlockModel,
BookmarkBlockModel,
CodeBlockModel,
DatabaseBlockModel,
ImageBlockModel,
ListBlockModel,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/affine-model';
import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/block-std';
import {
type AttachmentBlockModel,
type BookmarkBlockModel,
type CodeBlockModel,
type DatabaseBlockModel,
DocDisplayMetaProvider,
type ImageBlockModel,
type ListBlockModel,
type ParagraphBlockModel,
type RootBlockModel,
} from '@blocksuite/blocks';
import { noop, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { LinkedPageIcon } from '@blocksuite/icons/lit';
import type { DeltaInsert } from '@blocksuite/inline';
@@ -1,4 +1,5 @@
import type { ParagraphBlockModel, Signal } from '@blocksuite/blocks';
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import type { EditorHost } from '@blocksuite/block-std';
import {
AttachmentIcon,
BlockIcon,
@@ -20,10 +21,9 @@ import {
TextIcon,
} from '@blocksuite/icons/lit';
import { createContext } from '@lit/context';
import type { Signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
const _16px = { width: '16px', height: '16px' };
const paragraphIconMap: Record<
@@ -85,7 +85,7 @@ export const headingKeys = new Set(
export const outlineSettingsKey = 'outlinePanelSettings';
export type TocContext = {
editor$: Signal<AffineEditorContainer>;
editor$: Signal<EditorHost>;
enableSorting$: Signal<boolean>;
showIcons$: Signal<boolean>;
fitPadding$: Signal<number[]>;
@@ -0,0 +1,39 @@
import { AFFINE_OUTLINE_NOTICE, OutlineNotice } from './body/outline-notice';
import {
AFFINE_OUTLINE_PANEL_BODY,
OutlinePanelBody,
} from './body/outline-panel-body';
import { AFFINE_OUTLINE_NOTE_CARD, OutlineNoteCard } from './card/outline-card';
import {
AFFINE_OUTLINE_BLOCK_PREVIEW,
OutlineBlockPreview,
} from './card/outline-preview';
import {
AFFINE_OUTLINE_PANEL_HEADER,
OutlinePanelHeader,
} from './header/outline-panel-header';
import {
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu,
} from './header/outline-setting-menu';
import {
AFFINE_MOBILE_OUTLINE_MENU,
MobileOutlineMenu,
} from './mobile-outline-panel';
import { AFFINE_OUTLINE_PANEL, OutlinePanel } from './outline-panel';
import { AFFINE_OUTLINE_VIEWER, OutlineViewer } from './outline-viewer';
export function effects() {
customElements.define(
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu
);
customElements.define(AFFINE_OUTLINE_NOTICE, OutlineNotice);
customElements.define(AFFINE_OUTLINE_PANEL, OutlinePanel);
customElements.define(AFFINE_OUTLINE_PANEL_HEADER, OutlinePanelHeader);
customElements.define(AFFINE_OUTLINE_NOTE_CARD, OutlineNoteCard);
customElements.define(AFFINE_OUTLINE_VIEWER, OutlineViewer);
customElements.define(AFFINE_MOBILE_OUTLINE_MENU, MobileOutlineMenu);
customElements.define(AFFINE_OUTLINE_BLOCK_PREVIEW, OutlineBlockPreview);
customElements.define(AFFINE_OUTLINE_PANEL_BODY, OutlinePanelBody);
}
@@ -1,5 +1,5 @@
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { createButtonPopper } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { SettingsIcon, SortIcon } from '@blocksuite/icons/lit';
import { consume } from '@lit/context';
@@ -1,11 +1,16 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { PropTypes, requiredProperties } from '@blocksuite/block-std';
import {
matchModels,
NoteDisplayMode,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
type EditorHost,
PropTypes,
requiredProperties,
} from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
@@ -14,7 +19,6 @@ import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { getHeadingBlocksFromDoc } from './utils/query.js';
import {
observeActiveHeadingDuringScroll,
@@ -162,8 +166,9 @@ export class MobileOutlineMenu extends SignalWatcher(
};
override render() {
if (this.editor.doc.root === null || this.editor.mode === 'edgeless')
return nothing;
const docModeService = this.editor.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
if (this.editor.doc.root === null || mode === 'edgeless') return nothing;
const headingBlocks = getHeadingBlocksFromDoc(
this.editor.doc,
@@ -182,7 +187,7 @@ export class MobileOutlineMenu extends SignalWatcher(
}
@property({ attribute: false })
accessor editor!: AffineEditorContainer;
accessor editor!: EditorHost;
}
declare global {
@@ -1,4 +1,6 @@
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
type EditorHost,
PropTypes,
requiredProperties,
ShadowlessElement,
@@ -9,7 +11,6 @@ import { effect, signal } from '@preact/signals-core';
import { html, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { outlineSettingsKey, type TocContext, tocContext } from './config.js';
import * as styles from './outline-panel.css';
@@ -21,6 +22,12 @@ export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel';
export class OutlinePanel extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
private _getEditorMode(host: EditorHost) {
const docModeService = host.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode;
}
private _setContext() {
this._context = {
editor$: signal(this.editor),
@@ -39,7 +46,7 @@ export class OutlinePanel extends SignalWatcher(
}
const editor = this._context.editor$.value;
if (editor.mode === 'edgeless') {
if (this._getEditorMode(editor) === 'edgeless') {
this._context.enableSorting$.value = true;
} else if (settings) {
this._context.enableSorting$.value = settings.enableSorting;
@@ -51,7 +58,8 @@ export class OutlinePanel extends SignalWatcher(
private _watchSettingsChange() {
this.disposables.add(
effect(() => {
if (this._context.editor$.value.mode === 'edgeless') return;
if (this._getEditorMode(this._context.editor$.value) === 'edgeless')
return;
const showPreviewIcon = this._context.showIcons$.value;
const enableNotesSorting = this._context.enableSorting$.value;
@@ -84,7 +92,7 @@ export class OutlinePanel extends SignalWatcher(
}
override render() {
if (!this.editor.host) return;
if (!this.editor) return;
return html`
<affine-outline-panel-header></affine-outline-panel-header>
@@ -97,7 +105,7 @@ export class OutlinePanel extends SignalWatcher(
private accessor _context!: TocContext;
@property({ attribute: false })
accessor editor!: AffineEditorContainer;
accessor editor!: EditorHost;
@property({ attribute: false })
accessor fitPadding!: number[];
@@ -1,9 +1,12 @@
import { NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
import {
type EditorHost,
PropTypes,
requiredProperties,
ShadowlessElement,
} from '@blocksuite/block-std';
import { NoteDisplayMode, scrollbarStyle } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { TocIcon } from '@blocksuite/icons/lit';
import { provide } from '@lit/context';
@@ -13,7 +16,6 @@ import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { type TocContext, tocContext } from './config.js';
import { getHeadingBlocksFromDoc } from './utils/query.js';
import {
@@ -219,8 +221,9 @@ export class OutlineViewer extends SignalWatcher(
}
override render() {
if (this.editor.doc.root === null || this.editor.mode === 'edgeless')
return nothing;
const docModeService = this.editor.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
if (this.editor.doc.root === null || mode === 'edgeless') return nothing;
const headingBlocks = getHeadingBlocksFromDoc(
this.editor.doc,
@@ -308,7 +311,7 @@ export class OutlineViewer extends SignalWatcher(
private accessor _showViewer: boolean = false;
@property({ attribute: false })
accessor editor!: AffineEditorContainer;
accessor editor!: EditorHost;
@property({ attribute: false })
accessor toggleOutlinePanel: (() => void) | null = null;
@@ -1,4 +1,4 @@
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/blocks';
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
export type ReorderEvent = CustomEvent<{
currentNumber: number;
@@ -1,11 +1,10 @@
import {
BlocksUtils,
matchModels,
NoteBlockModel,
NoteDisplayMode,
ParagraphBlockModel,
RootBlockModel,
} from '@blocksuite/blocks';
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockModel, Store } from '@blocksuite/store';
import { headingKeys } from '../config.js';
@@ -35,14 +34,14 @@ export function getNotesFromDoc(
}
export function isRootBlock(block: BlockModel): block is RootBlockModel {
return BlocksUtils.matchModels(block, [RootBlockModel]);
return matchModels(block, [RootBlockModel]);
}
export function isHeadingBlock(
block: BlockModel
): block is ParagraphBlockModel {
return (
BlocksUtils.matchModels(block, [ParagraphBlockModel]) &&
matchModels(block, [ParagraphBlockModel]) &&
headingKeys.has(block.type$.value)
);
}
@@ -1,16 +1,18 @@
import { getDocTitleByEditorHost } from '@blocksuite/affine-components/doc-title';
import { NoteDisplayMode } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types';
import type { EditorHost } from '@blocksuite/block-std';
import { NoteDisplayMode } from '@blocksuite/blocks';
import { clamp, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '../../../editors/editor-container.js';
import { getDocTitleByEditorHost } from '../../doc-title/index.js';
import { getHeadingBlocksFromDoc } from './query.js';
export function scrollToBlock(editor: AffineEditorContainer, blockId: string) {
const { host, mode } = editor;
if (mode === 'edgeless' || !host) return;
export function scrollToBlock(host: EditorHost, blockId: string) {
const docModeService = host.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
if (mode === 'edgeless') return;
if (editor.doc.root?.id === blockId) {
if (host.doc.root?.id === blockId) {
const docTitle = getDocTitleByEditorHost(host);
if (!docTitle) return;
@@ -50,12 +52,11 @@ export function isBlockBeforeViewportCenter(
}
export const observeActiveHeadingDuringScroll = (
getEditor: () => AffineEditorContainer, // workaround for editor changed
getEditor: () => EditorHost, // workaround for editor changed
update: (activeHeading: string | null) => void
) => {
const handler = () => {
const { host } = getEditor();
if (!host) return;
const host = getEditor();
const headings = getHeadingBlocksFromDoc(
host.doc,
@@ -82,15 +83,14 @@ export const observeActiveHeadingDuringScroll = (
let highlightMask: HTMLDivElement | null = null;
let highlightTimeoutId: ReturnType<typeof setTimeout> | null = null;
function highlightBlock(editor: AffineEditorContainer, blockId: string) {
function highlightBlock(host: EditorHost, blockId: string) {
const emptyClear = () => {};
const { host } = editor;
if (!host) return emptyClear;
if (host.doc.root?.id === blockId) return emptyClear;
if (editor.doc.root?.id === blockId) return emptyClear;
const rootComponent = host.querySelector('affine-page-root');
const rootComponent = host.querySelector<
HTMLElement & { viewport: Viewport }
>('affine-page-root');
if (!rootComponent) return emptyClear;
if (!rootComponent.viewport) {
@@ -153,11 +153,11 @@ function highlightBlock(editor: AffineEditorContainer, blockId: string) {
// this function is useful when the scroll need smooth animation
let highlightIntervalId: ReturnType<typeof setInterval> | null = null;
export async function scrollToBlockWithHighlight(
editor: AffineEditorContainer,
host: EditorHost,
blockId: string,
timeout = 3000
) {
scrollToBlock(editor, blockId);
scrollToBlock(host, blockId);
let timeCount = 0;
@@ -174,10 +174,9 @@ export async function scrollToBlockWithHighlight(
return;
}
const { host } = editor;
const block = host?.view.getBlock(blockId);
const block = host.view.getBlock(blockId);
if (!host || !block || timeCount > timeout) {
if (!block || timeCount > timeout) {
clearInterval(highlightIntervalId);
resolve(() => {});
return;
@@ -195,7 +194,7 @@ export async function scrollToBlockWithHighlight(
clearInterval(highlightIntervalId);
// highlight block
resolve(highlightBlock(editor, blockId));
resolve(highlightBlock(host, blockId));
}, 100);
});
}
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../block-note" },
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
{ "path": "../../framework/block-std" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
{ "path": "../../framework/store" }
]
}
+1 -1
View File
@@ -17,7 +17,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"fractional-indexing": "^3.2.0",
"yjs": "^13.6.21",
"zod": "^3.23.8"
+2 -1
View File
@@ -22,9 +22,10 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"dompurify": "^3.2.4",
"fractional-indexing": "^3.2.0",
"lit": "^3.2.0",
"lodash.clonedeep": "^4.5.0",
@@ -5,6 +5,7 @@ import type { LinkToolbarEvents } from './link.js';
import type { NoteEvents } from './note.js';
import type {
AttachmentUploadedEvent,
BlockCreationEvent,
DocCreatedEvent,
ElementCreationEvent,
ElementLockEvent,
@@ -24,6 +25,7 @@ export type TelemetryEventMap = OutDatabaseAllEvents &
EdgelessElementLocked: ElementLockEvent;
ExpandedAndCollapsed: MindMapCollapseEvent;
AttachmentUploadedEvent: AttachmentUploadedEvent;
BlockCreated: BlockCreationEvent;
};
export interface TelemetryService {
@@ -51,6 +51,10 @@ export interface ElementLockEvent extends TelemetryEvent {
control: 'lock' | 'unlock' | 'group-lock';
}
export interface BlockCreationEvent extends TelemetryEvent {
blockType: string;
}
export interface MindMapCollapseEvent extends TelemetryEvent {
page: 'whiteboard editor';
segment: 'mind map';
@@ -0,0 +1,28 @@
import DOMPurify from 'dompurify';
export function readImageSize(file: File | Blob) {
return new Promise<{ width: number; height: number }>(resolve => {
const size = { width: 0, height: 0 };
if (!file.type.startsWith('image/')) {
resolve(size);
return;
}
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
URL.revokeObjectURL(img.src);
resolve(size);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
resolve(size);
};
const sanitizedURL = DOMPurify.sanitize(URL.createObjectURL(file));
img.src = sanitizedURL;
});
}
@@ -8,6 +8,7 @@ export * from './edgeless';
export * from './event';
export * from './file';
export * from './fractional-indexing';
export * from './image';
export * from './insert';
export * from './is-abort-error';
export * from './math';
@@ -1,8 +1,12 @@
import type { Viewport } from '@blocksuite/block-std/gfx';
import {
GfxControllerIdentifier,
type Viewport,
} from '@blocksuite/block-std/gfx';
import { Pane } from 'tweakpane';
import { getSentenceRects, segmentSentences } from './text-utils.js';
import type { ParagraphLayout, SectionLayout } from './types.js';
import type { ParagraphLayout, ViewportLayout } from './types.js';
import type { ViewportTurboRendererExtension } from './viewport-renderer.js';
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
const hostRect = host.getBoundingClientRect();
@@ -17,30 +21,30 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
canvas.style.pointerEvents = 'none';
}
export function getSectionLayout(
export function getViewportLayout(
host: HTMLElement,
viewport: Viewport
): SectionLayout {
): ViewportLayout {
const paragraphBlocks = host.querySelectorAll(
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
);
const zoom = viewport.zoom;
let sectionMinX = Infinity;
let sectionMinY = Infinity;
let sectionMaxX = -Infinity;
let sectionMaxY = -Infinity;
let layoutMinX = Infinity;
let layoutMinY = Infinity;
let layoutMaxX = -Infinity;
let layoutMaxY = -Infinity;
const paragraphs: ParagraphLayout[] = Array.from(paragraphBlocks).map(p => {
const sentences = segmentSentences(p.textContent || '');
const sentenceLayouts = sentences.map(sentence => {
const rects = getSentenceRects(p, sentence);
rects.forEach(({ rect }) => {
sectionMinX = Math.min(sectionMinX, rect.x);
sectionMinY = Math.min(sectionMinY, rect.y);
sectionMaxX = Math.max(sectionMaxX, rect.x + rect.w);
sectionMaxY = Math.max(sectionMaxY, rect.y + rect.h);
layoutMinX = Math.min(layoutMinX, rect.x);
layoutMinY = Math.min(layoutMinY, rect.y);
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
});
return {
text: sentence,
@@ -68,42 +72,59 @@ export function getSectionLayout(
};
});
const sectionModelCoord = viewport.toModelCoordFromClientCoord([
sectionMinX,
sectionMinY,
const layoutModelCoord = viewport.toModelCoordFromClientCoord([
layoutMinX,
layoutMinY,
]);
const w = (sectionMaxX - sectionMinX) / zoom / viewport.viewScale;
const h = (sectionMaxY - sectionMinY) / zoom / viewport.viewScale;
const section: SectionLayout = {
const w = (layoutMaxX - layoutMinX) / zoom / viewport.viewScale;
const h = (layoutMaxY - layoutMinY) / zoom / viewport.viewScale;
const layout: ViewportLayout = {
paragraphs,
rect: {
x: sectionModelCoord[0],
y: sectionModelCoord[1],
x: layoutModelCoord[0],
y: layoutModelCoord[1],
w: Math.max(w, 0),
h: Math.max(h, 0),
},
};
return section;
return layout;
}
export function initTweakpane(
viewportElement: HTMLElement,
onStateChange: (value: boolean) => void
renderer: ViewportTurboRendererExtension,
viewportElement: HTMLElement
) {
const debugPane = new Pane({ container: viewportElement });
const paneElement = debugPane.element;
paneElement.style.position = 'absolute';
paneElement.style.top = '10px';
paneElement.style.left = '10px';
paneElement.style.right = '10px';
paneElement.style.width = '250px';
debugPane.title = 'Viewport Turbo Renderer';
const params = { enabled: true };
debugPane
.addBinding(params, 'enabled', {
label: 'Enable',
.addBinding({ paused: true }, 'paused', {
label: 'Paused',
})
.on('change', ({ value }) => {
onStateChange(value);
renderer.state = value ? 'paused' : 'monitoring';
});
debugPane
.addBinding({ keepDOM: true }, 'keepDOM', {
label: 'Keep DOM',
})
.on('change', ({ value }) => {
const container = viewportElement.querySelector('gfx-viewport')!;
(container as HTMLElement).style.display = value ? 'block' : 'none';
});
debugPane.addButton({ title: 'Fit Viewport' }).on('click', () => {
const gfx = renderer.std.get(GfxControllerIdentifier);
gfx.fitToScreen();
});
debugPane.addButton({ title: 'Force Refresh' }).on('click', () => {
renderer.refresh(true).catch(console.error);
});
}
@@ -1,9 +1,9 @@
import { type SectionLayout } from './types.js';
import { type ViewportLayout } from './types.js';
type WorkerMessagePaint = {
type: 'paintSection';
type: 'paintLayout';
data: {
section: SectionLayout;
layout: ViewportLayout;
width: number;
height: number;
dpr: number;
@@ -38,20 +38,15 @@ function getBaseline() {
return y;
}
/** Section painter in worker */
class SectionPainter {
/** Layout painter in worker */
class LayoutPainter {
private readonly canvas: OffscreenCanvas = new OffscreenCanvas(0, 0);
private ctx: OffscreenCanvasRenderingContext2D | null = null;
private zoom = 1;
setSize(
sectionRectW: number,
sectionRectH: number,
dpr: number,
zoom: number
) {
const width = sectionRectW * dpr * zoom;
const height = sectionRectH * dpr * zoom;
setSize(layoutRectW: number, layoutRectH: number, dpr: number, zoom: number) {
const width = layoutRectW * dpr * zoom;
const height = layoutRectH * dpr * zoom;
this.canvas.width = width;
this.canvas.height = height;
@@ -68,11 +63,11 @@ class SectionPainter {
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
paint(section: SectionLayout) {
paint(layout: ViewportLayout) {
const { canvas, ctx } = this;
if (!canvas || !ctx) return;
if (section.rect.w === 0 || section.rect.h === 0) {
console.warn('empty section rect');
if (layout.rect.w === 0 || layout.rect.h === 0) {
console.warn('empty layout rect');
return;
}
@@ -83,7 +78,7 @@ class SectionPainter {
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
const renderedPositions = new Set<string>();
section.paragraphs.forEach(paragraph => {
layout.paragraphs.forEach(paragraph => {
const fontSize = 15;
ctx.font = `300 ${fontSize}px Inter`;
const baselineY = getBaseline();
@@ -91,8 +86,8 @@ class SectionPainter {
paragraph.sentences.forEach(sentence => {
ctx.strokeStyle = 'yellow';
sentence.rects.forEach(textRect => {
const x = textRect.rect.x - section.rect.x;
const y = textRect.rect.y - section.rect.y;
const x = textRect.rect.x - layout.rect.x;
const y = textRect.rect.y - layout.rect.y;
const posKey = `${x},${y}`;
// Only render if we haven't rendered at this position before
@@ -112,7 +107,7 @@ class SectionPainter {
}
}
const painter = new SectionPainter();
const painter = new LayoutPainter();
let fontLoaded = false;
font
@@ -131,10 +126,10 @@ self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
}
switch (type) {
case 'paintSection': {
const { section, width, height, dpr, zoom } = data;
case 'paintLayout': {
const { layout, width, height, dpr, zoom } = data;
painter.setSize(width, height, dpr, zoom);
painter.paint(section);
painter.paint(layout);
break;
}
}
@@ -8,13 +8,22 @@ interface WordSegment {
const CJK_REGEX = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/u;
const sentenceSegmenter = new Intl.Segmenter(undefined, {
granularity: 'sentence',
});
const wordSegmenter = new Intl.Segmenter(undefined, {
granularity: 'word',
});
const graphemeSegmenter = new Intl.Segmenter(undefined, {
granularity: 'grapheme',
});
function hasCJK(text: string): boolean {
return CJK_REGEX.test(text);
}
function getWordSegments(text: string): WordSegment[] {
const granularity = hasCJK(text) ? 'grapheme' : 'word';
const segmenter = new Intl.Segmenter(undefined, { granularity });
const segmenter = hasCJK(text) ? graphemeSegmenter : wordSegmenter;
return Array.from(segmenter.segment(text)).map(({ segment, index }) => ({
text: segment,
start: index,
@@ -130,6 +139,7 @@ export function getSentenceRects(
}
export function segmentSentences(text: string): string[] {
const segmenter = new Intl.Segmenter(undefined, { granularity: 'sentence' });
return Array.from(segmenter.segment(text)).map(({ segment }) => segment);
return Array.from(sentenceSegmenter.segment(text)).map(
({ segment }) => segment
);
}
@@ -23,7 +23,7 @@ export interface ParagraphLayout {
zoom: number;
}
export interface SectionLayout {
export interface ViewportLayout {
paragraphs: ParagraphLayout[];
rect: Rect;
}
@@ -6,15 +6,15 @@ import {
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di';
import { nextTick } from '@blocksuite/global/utils';
import { debounce, DisposableGroup } from '@blocksuite/global/utils';
import { type Pane } from 'tweakpane';
import {
getSectionLayout,
getViewportLayout,
initTweakpane,
syncCanvasSize,
} from './dom-utils.js';
import { type SectionLayout } from './types.js';
import { type ViewportLayout } from './types.js';
export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
'ViewportTurboRenderer'
@@ -22,10 +22,15 @@ export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
interface Tile {
bitmap: ImageBitmap;
zoom: number;
}
// With high enough zoom, fallback to DOM rendering
const zoomThreshold = 1;
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
state: 'monitoring' | 'paused' = 'paused';
disposables = new DisposableGroup();
static override setup(di: Container) {
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
@@ -33,8 +38,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
private readonly worker: Worker;
private lastZoom: number | null = null;
private lastSection: SectionLayout | null = null;
private layoutCache: ViewportLayout | null = null;
private tile: Tile | null = null;
private debugPane: Pane | null = null;
@@ -49,64 +53,88 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const viewportElement = document.querySelector('.affine-edgeless-viewport');
if (viewportElement) {
viewportElement.append(this.canvas);
initTweakpane(viewportElement as HTMLElement, (value: boolean) => {
this.state = value ? 'monitoring' : 'paused';
this.canvas.style.display = value ? 'block' : 'none';
});
initTweakpane(this, viewportElement as HTMLElement);
}
syncCanvasSize(this.canvas, this.std.host);
this.viewport.viewportUpdated.on(() => {
this.refresh().catch(console.error);
});
const debounceOptions = { leading: false, trailing: true };
const debouncedLayoutUpdate = debounce(
() => this.updateLayoutCache(),
500,
debounceOptions
);
this.disposables.add(
this.std.store.slots.blockUpdated.on(() => {
this.clearTile();
debouncedLayoutUpdate();
})
);
document.fonts.load('15px Inter').then(() => {
this.state = 'monitoring';
// this.state = 'monitoring';
this.refresh().catch(console.error);
});
}
override unmounted() {
if (this.tile) {
this.tile.bitmap.close();
this.tile = null;
}
this.clearTile();
if (this.debugPane) {
this.debugPane.dispose();
this.debugPane = null;
}
this.worker.terminate();
this.canvas.remove();
this.disposables.dispose();
}
get viewport() {
return this.std.get(GfxControllerIdentifier).viewport;
}
private async refresh() {
await nextTick(); // Improves stability during zooming
async refresh(force = false) {
if (this.state === 'paused' && !force) return;
if (this.canUseCache()) {
this.drawCachedBitmap(this.lastSection!);
if (this.viewport.zoom > zoomThreshold) {
this.clearCanvas();
} else if (this.canUseBitmapCache()) {
this.drawCachedBitmap(this.layoutCache!);
} else {
const section = getSectionLayout(this.std.host, this.viewport);
await this.paintSection(section);
this.lastSection = section;
this.lastZoom = this.viewport.zoom;
this.drawCachedBitmap(section);
// Unneeded most of the time, the DOM query is debounced after block update
if (!this.layoutCache) {
this.updateLayoutCache();
}
await this.paintLayout(this.layoutCache!);
this.drawCachedBitmap(this.layoutCache!);
}
}
private async paintSection(section: SectionLayout): Promise<void> {
private updateLayoutCache() {
const layout = getViewportLayout(this.std.host, this.viewport);
this.layoutCache = layout;
}
private clearTile() {
if (this.tile) {
this.tile.bitmap.close();
this.tile = null;
}
}
private async paintLayout(layout: ViewportLayout): Promise<void> {
return new Promise(resolve => {
if (!this.worker) return;
const dpr = window.devicePixelRatio;
this.worker.postMessage({
type: 'paintSection',
type: 'paintLayout',
data: {
section,
width: section.rect.w,
height: section.rect.h,
layout,
width: layout.rect.w,
height: layout.rect.h,
dpr,
zoom: this.viewport.zoom,
},
@@ -114,7 +142,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.onmessage = (e: MessageEvent) => {
if (e.data.type === 'bitmapPainted') {
this.handlePaintedBitmap(e.data.bitmap, section, resolve);
this.handlePaintedBitmap(e.data.bitmap, layout, resolve);
}
};
});
@@ -122,42 +150,49 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
private handlePaintedBitmap(
bitmap: ImageBitmap,
section: SectionLayout,
layout: ViewportLayout,
resolve: () => void
) {
if (this.tile) {
this.tile.bitmap.close();
}
this.tile = { bitmap };
this.drawCachedBitmap(section);
this.tile = {
bitmap,
zoom: this.viewport.zoom,
};
this.drawCachedBitmap(layout);
resolve();
}
private canUseCache(): boolean {
private canUseBitmapCache(): boolean {
return (
!!this.lastSection && !!this.tile && this.viewport.zoom === this.lastZoom
!!this.layoutCache && !!this.tile && this.viewport.zoom === this.tile.zoom
);
}
private drawCachedBitmap(section: SectionLayout) {
if (this.state === 'paused') return;
private clearCanvas() {
const ctx = this.canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
private drawCachedBitmap(layout: ViewportLayout) {
const bitmap = this.tile!.bitmap;
const ctx = this.canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const sectionViewCoord = this.viewport.toViewCoord(
section.rect.x,
section.rect.y
this.clearCanvas();
const layoutViewCoord = this.viewport.toViewCoord(
layout.rect.x,
layout.rect.y
);
ctx.drawImage(
bitmap,
sectionViewCoord[0] * window.devicePixelRatio,
sectionViewCoord[1] * window.devicePixelRatio,
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
section.rect.h * window.devicePixelRatio * this.viewport.zoom
layoutViewCoord[0] * window.devicePixelRatio,
layoutViewCoord[1] * window.devicePixelRatio,
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
);
}
}
@@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -22,7 +22,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0"
},
"exports": {
@@ -20,7 +20,7 @@
"@blocksuite/global": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0"
},
"exports": {
@@ -21,7 +21,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0"
},
"exports": {
@@ -18,7 +18,7 @@
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"lit": "^3.2.0"
},
"exports": {
+4 -2
View File
@@ -32,6 +32,8 @@
"@blocksuite/affine-block-surface-ref": "workspace:*",
"@blocksuite/affine-block-table": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
"@blocksuite/affine-fragment-outline": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-drag-handle": "workspace:*",
@@ -48,10 +50,10 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"@toeverything/theme": "^1.1.11",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0",
"dompurify": "^3.1.6",
"dompurify": "^3.2.4",
"fflate": "^0.8.2",
"file-type": "^20.0.0",
"fractional-indexing": "^3.2.0",
@@ -1 +0,0 @@
export type NavigatorMode = 'fill' | 'fit';
@@ -35,17 +35,30 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
const pathBlobIdMap = job.assetsManager.getPathBlobIdMap();
const assetsMap = job.assets;
await Promise.all(
// Add blobs to assets folder, if failed, log the error and continue
const results = await Promise.all(
Array.from(pathBlobIdMap.values()).map(async blobId => {
await job.assetsManager.readFromBlob(blobId);
const ext = getAssetName(assetsMap, blobId).split('.').at(-1);
const blob = assetsMap.get(blobId);
if (blob) {
await assets.file(`${blobId}.${ext}`, blob);
try {
await job.assetsManager.readFromBlob(blobId);
const ext = getAssetName(assetsMap, blobId).split('.').at(-1);
const blob = assetsMap.get(blobId);
if (blob) {
await assets.file(`${blobId}.${ext}`, blob);
return { success: true, blobId };
}
return { success: false, blobId, error: 'Blob not found' };
} catch (error) {
console.error(`Failed to process blob: ${blobId}`, error);
return { success: false, blobId, error };
}
})
);
const failures = results.filter(r => !r.success);
if (failures.length > 0) {
console.warn(`Failed to process ${failures.length} blobs:`, failures);
}
const downloadBlob = await zip.generate();
return download(downloadBlob, `${collection.id}.bs.zip`);
}

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