Compare commits

..

21 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
292 changed files with 1163 additions and 1366 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" }
]
@@ -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();
})
);
}
@@ -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() {
@@ -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'];
@@ -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;
@@ -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:*",
@@ -27,6 +28,7 @@
"@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
@@ -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"
@@ -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: {
@@ -135,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',
@@ -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" }
]
}
@@ -5,7 +5,7 @@ import {
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) {
@@ -21,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,
@@ -72,22 +72,22 @@ 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(
@@ -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;
}
}
@@ -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;
@@ -56,6 +60,19 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
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.refresh().catch(console.error);
@@ -63,16 +80,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}
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() {
@@ -82,30 +97,44 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
async refresh(force = false) {
if (this.state === 'paused' && !force) return;
await nextTick(); // Improves stability during zooming
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,
},
@@ -113,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);
}
};
});
@@ -121,40 +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) {
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
);
}
}
+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:*",
@@ -1 +0,0 @@
export type NavigatorMode = 'fill' | 'fit';
@@ -1,11 +1,12 @@
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
import type { ExtensionType } from '@blocksuite/store';
import { EdgelessRootBlockSpec } from '../../root-block/edgeless/edgeless-root-spec.js';
import {
EdgelessFrameManager,
FrameOverlay,
} from '../../root-block/edgeless/frame-manager.js';
} from '@blocksuite/affine-block-frame';
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
import { PresentTool } from '@blocksuite/affine-fragment-frame-panel';
import type { ExtensionType } from '@blocksuite/store';
import { EdgelessRootBlockSpec } from '../../root-block/edgeless/edgeless-root-spec.js';
import { BrushTool } from '../../root-block/edgeless/gfx-tool/brush-tool.js';
import { ConnectorTool } from '../../root-block/edgeless/gfx-tool/connector-tool.js';
import { CopilotTool } from '../../root-block/edgeless/gfx-tool/copilot-tool.js';
@@ -13,7 +14,6 @@ import { DefaultTool } from '../../root-block/edgeless/gfx-tool/default-tool.js'
import { MindMapIndicatorOverlay } from '../../root-block/edgeless/gfx-tool/default-tool-ext/mind-map-ext/indicator-overlay.js';
import { EmptyTool } from '../../root-block/edgeless/gfx-tool/empty-tool.js';
import { EraserTool } from '../../root-block/edgeless/gfx-tool/eraser-tool.js';
import { PresentTool } from '../../root-block/edgeless/gfx-tool/frame-navigator-tool.js';
import { FrameTool } from '../../root-block/edgeless/gfx-tool/frame-tool.js';
import { LassoTool } from '../../root-block/edgeless/gfx-tool/lasso-tool.js';
import { NoteTool } from '../../root-block/edgeless/gfx-tool/note-tool.js';
+9 -1
View File
@@ -22,6 +22,7 @@ import { effects as componentCaptionEffects } from '@blocksuite/affine-component
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
import { effects as componentDocTitleEffects } from '@blocksuite/affine-components/doc-title';
import { effects as componentDropIndicatorEffects } from '@blocksuite/affine-components/drop-indicator';
import { effects as componentEmbedCardModalEffects } from '@blocksuite/affine-components/embed-card-modal';
import { FilterableListComponent } from '@blocksuite/affine-components/filterable-list';
@@ -32,6 +33,8 @@ import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button';
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
import { effects as componentToolbarEffects } from '@blocksuite/affine-components/toolbar';
import { effects as fragmentFramePanelEffects } from '@blocksuite/affine-fragment-frame-panel/effects';
import { effects as fragmentOutlineEffects } from '@blocksuite/affine-fragment-outline/effects';
import { effects as widgetDragHandleEffects } from '@blocksuite/affine-widget-drag-handle/effects';
import { effects as widgetEdgelessAutoConnectEffects } from '@blocksuite/affine-widget-edgeless-auto-connect/effects';
import { effects as widgetFrameTitleEffects } from '@blocksuite/affine-widget-frame-title/effects';
@@ -184,6 +187,8 @@ export function effects() {
stdEffects();
inlineEffects();
dataViewEffects();
blockNoteEffects();
blockAttachmentEffects();
blockBookmarkEffects();
@@ -213,6 +218,7 @@ export function effects() {
componentAiItemEffects();
componentColorPickerEffects();
componentEmbedCardModalEffects();
componentDocTitleEffects();
widgetScrollAnchoringEffects();
widgetMobileToolbarEffects();
@@ -222,7 +228,9 @@ export function effects() {
widgetRemoteSelectionEffects();
widgetDragHandleEffects();
widgetEdgelessAutoConnectEffects();
dataViewEffects();
fragmentFramePanelEffects();
fragmentOutlineEffects();
customElements.define('affine-page-root', PageRootBlockComponent);
customElements.define('affine-preview-root', PreviewRootBlockComponent);
+6 -5
View File
@@ -7,7 +7,6 @@ import { splitElements } from './root-block/edgeless/utils/clipboard-utils.js';
import { isCanvasElement } from './root-block/edgeless/utils/query.js';
export * from './_common/adapters/index.js';
export { type NavigatorMode } from './_common/edgeless/frame/consts.js';
export * from './_common/transformers/index.js';
export * from './_specs/index.js';
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
@@ -16,10 +15,6 @@ export type {
TemplateCategory,
TemplateManager,
} from './root-block/edgeless/components/toolbar/template/template-type.js';
export {
EdgelessFrameManager,
FrameOverlay,
} from './root-block/edgeless/frame-manager.js';
export { CopilotTool } from './root-block/edgeless/gfx-tool/copilot-tool.js';
export * from './root-block/edgeless/gfx-tool/index.js';
export { EditPropsMiddlewareBuilder } from './root-block/edgeless/middlewares/base.js';
@@ -58,6 +53,10 @@ export {
type MenuOptions,
onMenuOpen,
} from '@blocksuite/affine-components/context-menu';
export {
DocTitle,
getDocTitleByEditorHost,
} from '@blocksuite/affine-components/doc-title';
export {
HoverController,
whenHover,
@@ -91,6 +90,8 @@ export {
ToolbarMoreMenuConfigExtension,
Tooltip,
} from '@blocksuite/affine-components/toolbar';
export * from '@blocksuite/affine-fragment-frame-panel';
export * from '@blocksuite/affine-fragment-outline';
export * from '@blocksuite/affine-model';
export {
AttachmentAdapter,
@@ -6,6 +6,11 @@ import {
SYNCED_MIN_HEIGHT,
SYNCED_MIN_WIDTH,
} from '@blocksuite/affine-block-embed';
import {
type EdgelessFrameManager,
type FrameOverlay,
isFrameBlock,
} from '@blocksuite/affine-block-frame';
import {
CanvasElementType,
isNoteBlock,
@@ -65,10 +70,6 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import type {
EdgelessFrameManager,
FrameOverlay,
} from '../../frame-manager.js';
import {
AI_CHAT_BLOCK_MAX_HEIGHT,
AI_CHAT_BLOCK_MAX_WIDTH,
@@ -90,7 +91,6 @@ import {
isEmbedLoomBlock,
isEmbedSyncedDocBlock,
isEmbedYoutubeBlock,
isFrameBlock,
isImageBlock,
isMindmapNode,
} from '../../utils/query.js';
@@ -1,3 +1,7 @@
import {
isFrameBlock,
type NavigatorMode,
} from '@blocksuite/affine-block-frame';
import { toast } from '@blocksuite/affine-components/toast';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { EditPropsStore } from '@blocksuite/affine-shared/services';
@@ -15,9 +19,7 @@ import { cssVar } from '@toeverything/theme';
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import type { NavigatorMode } from '../../../../_common/edgeless/frame/consts.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import { isFrameBlock } from '../../utils/query.js';
import { launchIntoFullscreen } from '../utils.js';
import { EdgelessToolbarToolMixin } from './mixins/tool.mixin.js';
@@ -1,3 +1,4 @@
import type { EdgelessFrameManager } from '@blocksuite/affine-block-frame';
import {
EdgelessCRUDIdentifier,
EdgelessLegacySlotIdentifier,
@@ -36,7 +37,6 @@ import { Bound, getCommonBound } from '@blocksuite/global/utils';
import { effect } from '@preact/signals-core';
import { RootService } from '../root-service.js';
import type { EdgelessFrameManager } from './frame-manager.js';
import { TemplateJob } from './services/template.js';
import {
createInsertPlaceMiddleware,
@@ -1,4 +1,9 @@
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
import {
type EdgelessFrameManager,
type FrameOverlay,
isFrameBlock,
} from '@blocksuite/affine-block-frame';
import {
ConnectorUtils,
isNoteBlock,
@@ -50,14 +55,9 @@ import { effect } from '@preact/signals-core';
import { isSingleMindMapNode } from '../../../_common/edgeless/mindmap/index.js';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
import type { EdgelessFrameManager, FrameOverlay } from '../frame-manager.js';
import { prepareCloneData } from '../utils/clone-utils.js';
import { calPanDelta } from '../utils/panning-utils.js';
import {
isCanvasElement,
isEdgelessTextBlock,
isFrameBlock,
} from '../utils/query.js';
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
import type { EdgelessSnapManager } from '../utils/snap-manager.js';
import {
addText,
@@ -1,3 +1,7 @@
import type {
EdgelessFrameManager,
FrameOverlay,
} from '@blocksuite/affine-block-frame';
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
@@ -15,8 +19,6 @@ import { Bound, Vec } from '@blocksuite/global/utils';
import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
import type { EdgelessFrameManager, FrameOverlay } from '../frame-manager.js';
export class FrameTool extends BaseTool {
static override toolName = 'frame';
@@ -4,7 +4,6 @@ export { CopilotTool } from './copilot-tool.js';
export { DefaultTool } from './default-tool.js';
export { EmptyTool } from './empty-tool.js';
export { EraserTool } from './eraser-tool.js';
export { PresentTool } from './frame-navigator-tool.js';
export { FrameTool } from './frame-tool.js';
export { LassoTool, type LassoToolOption } from './lasso-tool.js';
export { NoteTool, type NoteToolOption } from './note-tool.js';
@@ -1,3 +1,7 @@
import {
EdgelessFrameManager,
isFrameBlock,
} from '@blocksuite/affine-block-frame';
import { isNoteBlock } from '@blocksuite/affine-block-surface';
import type {
EdgelessTextBlockModel,
@@ -18,12 +22,10 @@ import { getCommonBoundWithRotation, groupBy } from '@blocksuite/global/utils';
import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
import { EdgelessFrameManager } from '../frame-manager.js';
import { getSortedCloneElements, prepareCloneData } from './clone-utils.js';
import {
isEdgelessTextBlock,
isEmbedSyncedDocBlock,
isFrameBlock,
isImageBlock,
} from './query.js';
@@ -1,5 +0,0 @@
export 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;
}
@@ -12,7 +12,6 @@ import {
type EmbedLoomModel,
type EmbedSyncedDocModel,
type EmbedYoutubeModel,
type FrameBlockModel,
type ImageBlockModel,
MindmapElementModel,
ShapeElementModel,
@@ -49,10 +48,6 @@ export function isEdgelessTextBlock(
);
}
export function isFrameBlock(element: unknown): element is FrameBlockModel {
return !!element && (element as BlockModel).flavour === 'affine:frame';
}
export function isImageBlock(
element: BlockModel | GfxModel | null
): element is ImageBlockModel {
@@ -85,10 +85,6 @@ export class AffineAIPanelWidget extends WidgetComponent {
private _answer: string | null = null;
private readonly _cancelCallback = () => {
this.focus();
};
private readonly _clearDiscardModal = () => {
if (this._discardModalAbort) {
this._discardModalAbort.abort();
@@ -97,25 +93,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
};
private readonly _clickOutside = () => {
switch (this.state) {
case 'hidden':
return;
case 'error':
case 'finished':
if (!this._answer) {
this.hide();
} else {
this.discard();
}
break;
default:
this.discard();
}
};
private readonly _discardCallback = () => {
this.hide();
this.config?.discardCallback?.();
this._discardWithConfirmation();
};
private _discardModalAbort: AbortController | null = null;
@@ -171,27 +149,27 @@ export class AffineAIPanelWidget extends WidgetComponent {
ctx: unknown = null;
discard = () => {
if ((this.state === 'finished' || this.state === 'error') && !this.answer) {
this._discardCallback();
private readonly _discardWithConfirmation = () => {
if (this.state === 'hidden') {
return;
}
if (this.state === 'input') {
if (this.state === 'input' || !this.answer) {
this.hide();
return;
}
this.showDiscardModal()
.then(discard => {
if (discard) {
this._discardCallback();
} else {
this._cancelCallback();
}
this.restoreSelection();
discard && this.discard();
})
.catch(console.error);
};
discard = () => {
this.hide();
this.restoreSelection();
this.config?.discardCallback?.();
};
/**
* You can evaluate this method multiple times to regenerate the answer.
*/
@@ -472,7 +450,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
'input',
() =>
html`<ai-panel-input
.onBlur=${this.discard}
.onBlur=${this._discardWithConfirmation}
.onFinish=${this._inputFinish}
.onInput=${this.onInput}
.networkSearchConfig=${config.networkSearchConfig}
@@ -1,3 +1,4 @@
import type { EdgelessFrameManager } from '@blocksuite/affine-block-frame';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import type {
EdgelessColorPickerButton,
@@ -36,7 +37,6 @@ import { join } from 'lit/directives/join.js';
import { when } from 'lit/directives/when.js';
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
import type { EdgelessFrameManager } from '../../edgeless/frame-manager.js';
import { mountFrameTitleEditor } from '../../edgeless/utils/text.js';
function getMostCommonColor(
@@ -1,3 +1,4 @@
import { isFrameBlock } from '@blocksuite/affine-block-frame';
import { isNoteBlock } from '@blocksuite/affine-block-surface';
import {
cloneGroups,
@@ -47,7 +48,6 @@ import {
isBookmarkBlock,
isEdgelessTextBlock,
isEmbeddedBlock,
isFrameBlock,
isImageBlock,
} from '../../edgeless/utils/query.js';
import { renderAddFrameButton } from './add-frame-button.js';
@@ -1,3 +1,4 @@
import { isFrameBlock } from '@blocksuite/affine-block-frame';
import {
isNoteBlock,
type SurfaceBlockComponent,
@@ -18,7 +19,6 @@ import {
isEmbeddedLinkBlock,
isEmbedLinkedDocBlock,
isEmbedSyncedDocBlock,
isFrameBlock,
isImageBlock,
} from '../../../edgeless/utils/query.js';
@@ -1,3 +1,4 @@
import { isFrameBlock } from '@blocksuite/affine-block-frame';
import { getSurfaceBlock, isNoteBlock } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel, NoteBlockModel } from '@blocksuite/affine-model';
import { NoteDisplayMode } from '@blocksuite/affine-model';
@@ -12,7 +13,6 @@ import {
mapFrameIds,
sortEdgelessElements,
} from '../../../edgeless/utils/clone-utils.js';
import { isFrameBlock } from '../../../edgeless/utils/query.js';
function addBlocksToDoc(targetDoc: Store, model: BlockModel, parentId: string) {
// Add current block to linked doc
+2
View File
@@ -25,6 +25,8 @@
{ "path": "../affine/block-surface-ref" },
{ "path": "../affine/block-table" },
{ "path": "../affine/components" },
{ "path": "../affine/fragment-frame-panel" },
{ "path": "../affine/fragment-outline" },
{ "path": "../affine/model" },
{ "path": "../affine/shared" },
{ "path": "../affine/widget-drag-handle" },
@@ -3,7 +3,11 @@ import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { PropTypes, requiredProperties } from '../view/decorators/required.js';
import { type EditorHost, ShadowlessElement } from '../view/index.js';
import {
type BlockComponent,
type EditorHost,
ShadowlessElement,
} from '../view/index.js';
import type { GfxBlockElementModel } from './model/gfx-block-model.js';
import { Viewport } from './viewport.js';
@@ -31,6 +35,13 @@ export function requestThrottledConnectedFrame<
}) as T;
}
function setDisplay(view: BlockComponent | null, display: 'block' | 'none') {
if (!view) return;
if (view.style.display !== display) {
view.style.display = display;
}
}
@requiredProperties({
viewport: PropTypes.instanceOf(Viewport),
})
@@ -53,10 +64,7 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
modelsInViewport.forEach(model => {
const view = host.std.view.getBlock(model.id);
if (view) {
view.style.display = '';
}
setDisplay(view, 'block');
if (this._lastVisibleModels?.has(model)) {
this._lastVisibleModels!.delete(model);
@@ -65,10 +73,7 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
this._lastVisibleModels?.forEach(model => {
const view = host.std.view.getBlock(model.id);
if (view) {
view.style.display = 'none';
}
setDisplay(view, 'none');
});
this._lastVisibleModels = modelsInViewport;
@@ -1,7 +1,7 @@
// because AbortSignal.throwIfAborted is not available in abortcontroller-polyfill
export function throwIfAborted(abort?: AbortSignal) {
if (abort?.aborted) {
throw new Error(abort.reason);
throw abort.reason;
}
return true;
}
+3
View File
@@ -0,0 +1,3 @@
# `@blocksuite/integration-test`
Integration test for BlockSuite.
@@ -1,6 +1,6 @@
{
"name": "@blocksuite/presets",
"description": "Prebuilt BlockSuite editors and opt-in additional UI components.",
"name": "@blocksuite/integration-test",
"description": "Integration test for BlockSuite",
"type": "module",
"scripts": {
"dev": "vite",

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