mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-03 10:40:44 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b15a2a9638 | |||
| f3218ab3bc | |||
| f79324b6a1 | |||
| adcc6b578c | |||
| 7f833f8c15 | |||
| 785951bbfa | |||
| 19e9f970f4 | |||
| c362737441 | |||
| 618042812b | |||
| 0ff7c5f897 | |||
| b8dcb85007 | |||
| 5ac15f12e6 | |||
| efe36161e8 | |||
| 126677d7ad | |||
| 007bbabce4 | |||
| 64cc99354e | |||
| 1516903c77 | |||
| 4f831732e1 | |||
| ef28e36441 | |||
| 7b1dfb7ee8 | |||
| 5fcc402280 | |||
| fa86f71853 | |||
| 4fee2a9c4b | |||
| b4097aef8e | |||
| 9f4311f654 | |||
| 13f1859cdf | |||
| 50820482df | |||
| ec67d30b27 | |||
| fd5897dbe6 | |||
| d490e767eb | |||
| adc003862b | |||
| ff0ce1a962 | |||
| 5042d9f644 | |||
| 1d339c682b | |||
| b38abcb59c | |||
| c3fc0a0d88 | |||
| e0b2b2b33c | |||
| ba91b36cc3 | |||
| a0e3f9909c | |||
| 8f6ce2c3b4 | |||
| d8435421d2 | |||
| 676ccc9094 | |||
| 091ba7bb51 | |||
| 2b11941c0e | |||
| 02f567f2c0 | |||
| 54b7515167 | |||
| e726df9a1b | |||
| 926b35c91f | |||
| b456feee63 |
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+13
-4
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
@@ -229,6 +229,10 @@ export class AffineLatexNode extends SignalWatcher(
|
||||
latex,
|
||||
}
|
||||
);
|
||||
this.editor.setInlineRange({
|
||||
index: this.endOffset,
|
||||
length: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+18
-19
@@ -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,
|
||||
});
|
||||
+2
-1
@@ -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
-1
@@ -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';
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
+15
-15
@@ -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();
|
||||
+6
-9
@@ -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
-2
@@ -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;
|
||||
};
|
||||
+2
-1
@@ -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"
|
||||
}
|
||||
+17
-32
@@ -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
|
||||
+2
-5
@@ -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';
|
||||
+11
-11
@@ -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';
|
||||
+4
-4
@@ -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
-1
@@ -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';
|
||||
+13
-8
@@ -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 {
|
||||
+13
-5
@@ -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[];
|
||||
+8
-5
@@ -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
-1
@@ -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;
|
||||
+4
-5
@@ -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)
|
||||
);
|
||||
}
|
||||
+21
-22
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user